diff --git a/.claude/skills/truthmark-check/SKILL.md b/.claude/skills/truthmark-check/SKILL.md index 8ffb143..2c9d6c2 100644 --- a/.claude/skills/truthmark-check/SKILL.md +++ b/.claude/skills/truthmark-check/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-check 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. argument-hint: Optional area, doc path, or audit focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Check diff --git a/.claude/skills/truthmark-document/SKILL.md b/.claude/skills/truthmark-document/SKILL.md index 5296072..c123523 100644 --- a/.claude/skills/truthmark-document/SKILL.md +++ b/.claude/skills/truthmark-document/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-document 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. argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Document @@ -45,6 +45,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no truth doc changed, report why current truth was already sufficient or why documentation was blocked +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template. Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior. Align existing docs to that template while preserving accurate authored content. diff --git a/.claude/skills/truthmark-realize/SKILL.md b/.claude/skills/truthmark-realize/SKILL.md index 9e4e940..5f34ff2 100644 --- a/.claude/skills/truthmark-realize/SKILL.md +++ b/.claude/skills/truthmark-realize/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-realize 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. argument-hint: Optional truth doc path, area, or desired code behavior to realize user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Realize diff --git a/.claude/skills/truthmark-structure/SKILL.md b/.claude/skills/truthmark-structure/SKILL.md index ce6508b..339d87f 100644 --- a/.claude/skills/truthmark-structure/SKILL.md +++ b/.claude/skills/truthmark-structure/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-structure 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. argument-hint: Optional area, directory, or routing concern user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- Use this skill to design or repair Truthmark area structure. diff --git a/.claude/skills/truthmark-sync/SKILL.md b/.claude/skills/truthmark-sync/SKILL.md index a799271..4227894 100644 --- a/.claude/skills/truthmark-sync/SKILL.md +++ b/.claude/skills/truthmark-sync/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-sync 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. argument-hint: Optional changed-code area, truth-doc area, or sync focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- 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. @@ -59,6 +59,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no impacted doc changed, report why truth was already current or why sync was skipped +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/.codex/skills/truthmark-check/SKILL.md b/.codex/skills/truthmark-check/SKILL.md index 8ffb143..2c9d6c2 100644 --- a/.codex/skills/truthmark-check/SKILL.md +++ b/.codex/skills/truthmark-check/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-check 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. argument-hint: Optional area, doc path, or audit focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Check diff --git a/.codex/skills/truthmark-check/agents/openai.yaml b/.codex/skills/truthmark-check/agents/openai.yaml index 74a948a..bf63c58 100644 --- a/.codex/skills/truthmark-check/agents/openai.yaml +++ b/.codex/skills/truthmark-check/agents/openai.yaml @@ -7,5 +7,5 @@ policy: allow_implicit_invocation: false truthmark: - version: "1.2.4" + version: "1.3.0" refresh_command: "truthmark init" diff --git a/.codex/skills/truthmark-document/SKILL.md b/.codex/skills/truthmark-document/SKILL.md index 5296072..c123523 100644 --- a/.codex/skills/truthmark-document/SKILL.md +++ b/.codex/skills/truthmark-document/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-document 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. argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Document @@ -45,6 +45,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no truth doc changed, report why current truth was already sufficient or why documentation was blocked +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template. Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior. Align existing docs to that template while preserving accurate authored content. diff --git a/.codex/skills/truthmark-document/agents/openai.yaml b/.codex/skills/truthmark-document/agents/openai.yaml index 3bc6756..1aeb4fc 100644 --- a/.codex/skills/truthmark-document/agents/openai.yaml +++ b/.codex/skills/truthmark-document/agents/openai.yaml @@ -7,5 +7,5 @@ policy: allow_implicit_invocation: false truthmark: - version: "1.2.4" + version: "1.3.0" refresh_command: "truthmark init" diff --git a/.codex/skills/truthmark-realize/SKILL.md b/.codex/skills/truthmark-realize/SKILL.md index 9e4e940..5f34ff2 100644 --- a/.codex/skills/truthmark-realize/SKILL.md +++ b/.codex/skills/truthmark-realize/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-realize 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. argument-hint: Optional truth doc path, area, or desired code behavior to realize user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Realize diff --git a/.codex/skills/truthmark-realize/agents/openai.yaml b/.codex/skills/truthmark-realize/agents/openai.yaml index 00ccc64..812ba8d 100644 --- a/.codex/skills/truthmark-realize/agents/openai.yaml +++ b/.codex/skills/truthmark-realize/agents/openai.yaml @@ -7,5 +7,5 @@ policy: allow_implicit_invocation: false truthmark: - version: "1.2.4" + version: "1.3.0" refresh_command: "truthmark init" diff --git a/.codex/skills/truthmark-structure/SKILL.md b/.codex/skills/truthmark-structure/SKILL.md index ce6508b..339d87f 100644 --- a/.codex/skills/truthmark-structure/SKILL.md +++ b/.codex/skills/truthmark-structure/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-structure 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. argument-hint: Optional area, directory, or routing concern user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- Use this skill to design or repair Truthmark area structure. diff --git a/.codex/skills/truthmark-structure/agents/openai.yaml b/.codex/skills/truthmark-structure/agents/openai.yaml index 96531b2..2931879 100644 --- a/.codex/skills/truthmark-structure/agents/openai.yaml +++ b/.codex/skills/truthmark-structure/agents/openai.yaml @@ -7,5 +7,5 @@ policy: allow_implicit_invocation: false truthmark: - version: "1.2.4" + version: "1.3.0" refresh_command: "truthmark init" diff --git a/.codex/skills/truthmark-sync/SKILL.md b/.codex/skills/truthmark-sync/SKILL.md index a799271..4227894 100644 --- a/.codex/skills/truthmark-sync/SKILL.md +++ b/.codex/skills/truthmark-sync/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-sync 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. argument-hint: Optional changed-code area, truth-doc area, or sync focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- 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. @@ -59,6 +59,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no impacted doc changed, report why truth was already current or why sync was skipped +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/.codex/skills/truthmark-sync/agents/openai.yaml b/.codex/skills/truthmark-sync/agents/openai.yaml index b65eb20..54eeff8 100644 --- a/.codex/skills/truthmark-sync/agents/openai.yaml +++ b/.codex/skills/truthmark-sync/agents/openai.yaml @@ -7,5 +7,5 @@ policy: allow_implicit_invocation: true truthmark: - version: "1.2.4" + version: "1.3.0" refresh_command: "truthmark init" diff --git a/.gemini/commands/truthmark/check.toml b/.gemini/commands/truthmark/check.toml index 4aec8ad..fff52ea 100644 --- a/.gemini/commands/truthmark/check.toml +++ b/.gemini/commands/truthmark/check.toml @@ -5,7 +5,7 @@ name: truthmark-check 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. argument-hint: Optional area, doc path, or audit focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Check diff --git a/.gemini/commands/truthmark/document.toml b/.gemini/commands/truthmark/document.toml index 04b2050..1c80c47 100644 --- a/.gemini/commands/truthmark/document.toml +++ b/.gemini/commands/truthmark/document.toml @@ -5,7 +5,7 @@ name: truthmark-document 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. argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Document @@ -47,6 +47,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no truth doc changed, report why current truth was already sufficient or why documentation was blocked +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template. Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior. Align existing docs to that template while preserving accurate authored content. diff --git a/.gemini/commands/truthmark/realize.toml b/.gemini/commands/truthmark/realize.toml index b83b082..8a53599 100644 --- a/.gemini/commands/truthmark/realize.toml +++ b/.gemini/commands/truthmark/realize.toml @@ -5,7 +5,7 @@ name: truthmark-realize 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. argument-hint: Optional truth doc path, area, or desired code behavior to realize user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Realize diff --git a/.gemini/commands/truthmark/structure.toml b/.gemini/commands/truthmark/structure.toml index bec8da7..910a13e 100644 --- a/.gemini/commands/truthmark/structure.toml +++ b/.gemini/commands/truthmark/structure.toml @@ -5,7 +5,7 @@ name: truthmark-structure 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. argument-hint: Optional area, directory, or routing concern user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- Use this skill to design or repair Truthmark area structure. diff --git a/.gemini/commands/truthmark/sync.toml b/.gemini/commands/truthmark/sync.toml index 8a76814..6832403 100644 --- a/.gemini/commands/truthmark/sync.toml +++ b/.gemini/commands/truthmark/sync.toml @@ -5,7 +5,7 @@ name: truthmark-sync 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. argument-hint: Optional changed-code area, truth-doc area, or sync focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- 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. @@ -61,6 +61,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no impacted doc changed, report why truth was already current or why sync was skipped +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e459c92..bea23c3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,7 +1,7 @@ ## Truthmark Workflow -Generated by Truthmark 1.2.4. Rerun `truthmark init` after upgrades. +Generated by Truthmark 1.3.0. Rerun `truthmark init` after upgrades. Hierarchy: config .truthmark/config.yml; routes docs/truthmark/areas.md and docs/truthmark/areas/**/*.md; Truth docs: docs/truth/**/*.md. Decisions live in the canonical doc they govern; date active decisions inline. Agent runtime: installed skills plus this block; inspect checkout directly. Delegation is host-owned. diff --git a/.github/prompts/truthmark-check.prompt.md b/.github/prompts/truthmark-check.prompt.md index 7816797..880c576 100644 --- a/.github/prompts/truthmark-check.prompt.md +++ b/.github/prompts/truthmark-check.prompt.md @@ -8,7 +8,7 @@ name: truthmark-check 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. argument-hint: Optional area, doc path, or audit focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Check diff --git a/.github/prompts/truthmark-document.prompt.md b/.github/prompts/truthmark-document.prompt.md index c6055b3..80f5946 100644 --- a/.github/prompts/truthmark-document.prompt.md +++ b/.github/prompts/truthmark-document.prompt.md @@ -8,7 +8,7 @@ name: truthmark-document 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. argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Document @@ -50,6 +50,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no truth doc changed, report why current truth was already sufficient or why documentation was blocked +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template. Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior. Align existing docs to that template while preserving accurate authored content. diff --git a/.github/prompts/truthmark-realize.prompt.md b/.github/prompts/truthmark-realize.prompt.md index 6eec353..268625e 100644 --- a/.github/prompts/truthmark-realize.prompt.md +++ b/.github/prompts/truthmark-realize.prompt.md @@ -8,7 +8,7 @@ name: truthmark-realize 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. argument-hint: Optional truth doc path, area, or desired code behavior to realize user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Realize diff --git a/.github/prompts/truthmark-structure.prompt.md b/.github/prompts/truthmark-structure.prompt.md index f859133..4fffc87 100644 --- a/.github/prompts/truthmark-structure.prompt.md +++ b/.github/prompts/truthmark-structure.prompt.md @@ -8,7 +8,7 @@ name: truthmark-structure 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. argument-hint: Optional area, directory, or routing concern user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- Use this skill to design or repair Truthmark area structure. diff --git a/.github/prompts/truthmark-sync.prompt.md b/.github/prompts/truthmark-sync.prompt.md index 9609f6a..6e20d07 100644 --- a/.github/prompts/truthmark-sync.prompt.md +++ b/.github/prompts/truthmark-sync.prompt.md @@ -8,7 +8,7 @@ name: truthmark-sync 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. argument-hint: Optional changed-code area, truth-doc area, or sync focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- 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. @@ -64,6 +64,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no impacted doc changed, report why truth was already current or why sync was skipped +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/.opencode/skills/truthmark-check/SKILL.md b/.opencode/skills/truthmark-check/SKILL.md index 8ffb143..2c9d6c2 100644 --- a/.opencode/skills/truthmark-check/SKILL.md +++ b/.opencode/skills/truthmark-check/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-check 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. argument-hint: Optional area, doc path, or audit focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Check diff --git a/.opencode/skills/truthmark-document/SKILL.md b/.opencode/skills/truthmark-document/SKILL.md index 5296072..c123523 100644 --- a/.opencode/skills/truthmark-document/SKILL.md +++ b/.opencode/skills/truthmark-document/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-document 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. argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Document @@ -45,6 +45,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no truth doc changed, report why current truth was already sufficient or why documentation was blocked +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template. Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior. Align existing docs to that template while preserving accurate authored content. diff --git a/.opencode/skills/truthmark-realize/SKILL.md b/.opencode/skills/truthmark-realize/SKILL.md index 9e4e940..5f34ff2 100644 --- a/.opencode/skills/truthmark-realize/SKILL.md +++ b/.opencode/skills/truthmark-realize/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-realize 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. argument-hint: Optional truth doc path, area, or desired code behavior to realize user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- # Truthmark Realize diff --git a/.opencode/skills/truthmark-structure/SKILL.md b/.opencode/skills/truthmark-structure/SKILL.md index ce6508b..339d87f 100644 --- a/.opencode/skills/truthmark-structure/SKILL.md +++ b/.opencode/skills/truthmark-structure/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-structure 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. argument-hint: Optional area, directory, or routing concern user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- Use this skill to design or repair Truthmark area structure. diff --git a/.opencode/skills/truthmark-sync/SKILL.md b/.opencode/skills/truthmark-sync/SKILL.md index a799271..4227894 100644 --- a/.opencode/skills/truthmark-sync/SKILL.md +++ b/.opencode/skills/truthmark-sync/SKILL.md @@ -3,7 +3,7 @@ name: truthmark-sync 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. argument-hint: Optional changed-code area, truth-doc area, or sync focus user-invocable: true -truthmark-version: 1.2.4 +truthmark-version: 1.3.0 --- 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. @@ -59,6 +59,9 @@ Evidence Gate: - tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts - remove, narrow, or block unsupported claims - if no impacted doc changed, report why truth was already current or why sync was skipped +Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available. +They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries. +If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated. Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/AGENTS.md b/AGENTS.md index 77a4cd1..d4bad39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ Agent-specific: ## Truthmark Workflow -Generated by Truthmark 1.2.4. Rerun `truthmark init` after upgrades. +Generated by Truthmark 1.3.0. Rerun `truthmark init` after upgrades. Hierarchy: config .truthmark/config.yml; routes docs/truthmark/areas.md and docs/truthmark/areas/**/*.md; Truth docs: docs/truth/**/*.md. Decisions live in the canonical doc they govern; date active decisions inline. Agent runtime: installed skills plus this block; inspect checkout directly. Delegation is host-owned. diff --git a/CLAUDE.md b/CLAUDE.md index 77a4cd1..d4bad39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Agent-specific: ## Truthmark Workflow -Generated by Truthmark 1.2.4. Rerun `truthmark init` after upgrades. +Generated by Truthmark 1.3.0. Rerun `truthmark init` after upgrades. Hierarchy: config .truthmark/config.yml; routes docs/truthmark/areas.md and docs/truthmark/areas/**/*.md; Truth docs: docs/truth/**/*.md. Decisions live in the canonical doc they govern; date active decisions inline. Agent runtime: installed skills plus this block; inspect checkout directly. Delegation is host-owned. diff --git a/GEMINI.md b/GEMINI.md index 77a4cd1..d4bad39 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -9,7 +9,7 @@ Agent-specific: ## Truthmark Workflow -Generated by Truthmark 1.2.4. Rerun `truthmark init` after upgrades. +Generated by Truthmark 1.3.0. Rerun `truthmark init` after upgrades. Hierarchy: config .truthmark/config.yml; routes docs/truthmark/areas.md and docs/truthmark/areas/**/*.md; Truth docs: docs/truth/**/*.md. Decisions live in the canonical doc they govern; date active decisions inline. Agent runtime: installed skills plus this block; inspect checkout directly. Delegation is host-owned. diff --git a/README.de.md b/README.de.md index 4cbefd5..e95befe 100644 --- a/README.de.md +++ b/README.de.md @@ -150,34 +150,65 @@ Bestehende Repositories brauchen nach `init` meist einen Aufräumschritt: Führe Truthmark ist am stärksten auf dem Standardpfad, nicht als Sammlung manueller Befehle. Der handelnde Agent und die Host-Umgebung entscheiden, ob delegiert oder der installierte Workflow inline ausgeführt wird. -Nutze Truth Document, wenn implementiertes Verhalten bereits existiert, aber die kanonischen Wahrheitsdokumente fehlen oder schwach sind. Der Agent liest Implementierung, Tests, Routing und vorhandene Dokumente, schreibt nur Wahrheitsdokumente und Routing und darf funktionalen Code nicht ändern. Codex, Claude Code und unterstützte Copilot-IDEs können es mit `/truthmark-document` aufrufen; Hosts im OpenCode-Stil können `/skill truthmark-document` verwenden. +### Vorhandenes Verhalten ohne Doku + +Nutze das, wenn die Implementierung bereits existiert, aber die kanonischen Wahrheitsdokumente fehlen oder schwach sind: + +```text +benutzer identifiziert ein implementiertes verhalten oder einen api-endpunkt +benutzer ruft truth document ausdrücklich auf +agent liest implementierung, tests, routing und vorhandene docs +agent schreibt nur truth docs und routing +truth-doc-diff prüfen +``` + +Truth Document ist manuell und implementation-first: Code dient als Beleg, Wahrheitsdokumente werden erstellt oder repariert, und funktionaler Code darf nicht geändert werden. Codex, Claude Code und unterstützte Copilot-IDEs können es mit `/truthmark-document` aufrufen. Hosts im OpenCode-Stil können `/skill truthmark-document` verwenden. + +```text +/truthmark-document dokumentiere das implementierte session-timeout-verhalten unter docs/truth/authentication +``` + +### Normale Codeänderungen Die meisten Nutzer sollten Truth Sync nicht direkt aufrufen müssen. Entscheidend ist, dass der installierte Agenten-Workflow Truth Sync als Abschlusskontrolle behandelt, wenn funktionaler Code geändert wurde. Der normale Ablauf ist: ```text -Agent ändert funktionalen Code -relevante Tests laufen -der installierte Truth-Sync-Workflow läuft vor dem Abschluss des Agenten -Truth-Doc-Diff prüfen, falls einer erzeugt wurde -Arbeit committen oder übergeben +agent ändert funktionalen code +relevante tests laufen +der installierte truth-sync-workflow läuft, bevor der agent fertig ist +truth-doc-diff prüfen, falls einer erzeugt wurde +arbeit committen oder übergeben ``` Truth Sync ist code-first: Code führt, Wahrheitsdokumente folgen, und Truth Sync darf funktionalen Code nicht umschreiben. Seine Hauptaufgabe ist, über den installierten Agenten-Workflow als Abschlusskontrolle zu laufen, wenn funktionaler Code geändert wurde. Direkte Aufrufe sind vor allem für Fehlersuche, frühe Synchronisierung vor einer Übergabe oder bewusstes Ausführen des Workflows gedacht. + Codex, Claude Code und unterstützte Copilot-IDEs können es mit `/truthmark-sync` aufrufen. Hosts im OpenCode-Stil können `/skill truthmark-sync` verwenden. -Nutze diesen Ablauf, wenn eine Produkt- oder Architekturentscheidung in der Dokumentation beginnt: ```text -Benutzer bearbeitet Wahrheitsdokumente -Benutzer ruft Truth Realize ausdrücklich auf -Agent liest Wahrheitsdokumente und relevanten Code -Agent aktualisiert nur Code -relevante Tests laufen -Arbeit committen oder übergeben +/truthmark-sync die repository-wahrheit jetzt vor der übergabe synchronisieren +``` + +### Doc-first-Änderungen + +Nutze das, wenn eine Produkt- oder Architekturentscheidung in der Doku beginnt: + +```text +benutzer bearbeitet wahrheitsdokumente +benutzer ruft truth realize ausdrücklich auf +agent liest wahrheitsdokumente und relevanten code +agent aktualisiert nur code +relevante tests laufen +arbeit committen oder übergeben ``` Truth Realize ist manuell und doc-first: Wahrheitsdokumente führen, Code folgt, und der Agent darf die Wahrheitsdokumente, die er realisiert, nicht bearbeiten. + Codex, Claude Code und unterstützte Copilot-IDEs können es mit `/truthmark-realize` aufrufen. Hosts im OpenCode-Stil können `/skill truthmark-realize` verwenden. +```text +/truthmark-realize docs/truth/authentication/session-timeout.md in code umsetzen +``` + ## Was es installiert Truthmark hält die dauerhafte Workflow-Fläche klein und repository-nativ. Nach `truthmark init` trägt das Repository selbst Routing, Regeln und installierte Workflow-Flächen, sodass Teams nicht nur auf die lokale Konfiguration einer einzelnen Person angewiesen sind. @@ -203,21 +234,42 @@ Generierte Flächen werden von Truthmark verwaltet, enthalten einen Versionsmark ## Befehle -Truthmark V1 hält die CLI absichtlich klein, weil der laufende Workflow in den installierten Agenten-Flächen leben soll und nicht in einer langen Liste täglicher manueller Befehle. In nachgelagerten Repositories erzeugt `truthmark config` den in Git festgeschriebenen Hierarchievertrag, `truthmark init` installiert und aktualisiert Workflow-Flächen aus dieser geprüften Konfiguration, und `truthmark check` validiert Wahrheitsartefakte für manuelle Audits, CI oder Fehlersuche. +Truthmark V1 hält die CLI fokussiert, weil der laufende Workflow in den installierten Agenten-Flächen leben soll und nicht in einer langen Liste täglicher manueller Befehle. In nachgelagerten Repositories erzeugt `truthmark config` den in Git festgeschriebenen Hierarchievertrag, `truthmark init` installiert und aktualisiert Workflow-Flächen aus dieser geprüften Konfiguration, `truthmark check` validiert Wahrheitsartefakte für manuelle Audits, CI oder Fehlersuche, und die Repository-Intelligence-Befehle erzeugen abgeleitete Prüfarbeitsstände, wenn lokale Werkzeuge verfügbar sind. ```bash truthmark config truthmark init truthmark check +truthmark index +truthmark impact --base main +truthmark context --workflow truth-sync --base main truthmark config --json truthmark check --json +truthmark index --json +truthmark impact --base main --json +truthmark context --workflow truth-sync --base main --json ``` `config` schreibt nur `.truthmark/config.yml`, außer `--stdout` wird verwendet. + `init` benötigt `.truthmark/config.yml` und installiert oder aktualisiert anschließend die lokalen Workflow-Dateien. + `check` validiert Konfiguration, Autorität, Routing, entscheidungstragende Dokumente, Frontmatter, interne Links, Branch-Scope und Coverage-Diagnostik. + +`index` baut RepoIndex- und RouteMap-JSON für den aktiven Checkout. + +`impact --base ` ordnet geänderte Dateien den gerouteten Truth-Dokumenten, Routen, nahen Tests und öffentlichen Symbolen zu. + +`context --workflow [--base ]` erzeugt ein begrenztes ContextPack für Truth Sync, Truth Document oder Truth Realize. `--format markdown` rendert eine menschenlesbare Fassung. + Truth Structure, Truth Document, Truth Sync, Truth Realize und Truth Check sind installierte Agenten-Workflows, keine täglichen Top-Level-CLI-Befehle. +Sie laufen über die konfigurierten Agenten-Host-Flächen, zum Beispiel Codex/Claude/Copilot `/truthmark-*`, OpenCode `/skill truthmark-*` oder Gemini `/truthmark:*`. + +```text +/truthmark-check routing und truth-abdeckung vor der review prüfen +``` + ## Warum es existiert Die meisten KI-Coding-Workflows optimieren für die nächste Antwort. Truthmark optimiert für die nächste Übergabe. @@ -235,15 +287,20 @@ Es geht davon aus, dass ernsthafte Teams Folgendes brauchen: ## Projektstatus Truthmark ist kein Memory-Server und kein MCP-Server. Es ist eine Repository-Praxis, verpackt als kleiner CLI-Installer plus agent-native Workflow-Flächen, die KI-Workflow-Regeln in Repository-Infrastruktur verwandeln. + V1 bietet derzeit: - `truthmark config` - `truthmark init` - `truthmark check` +- `truthmark index` +- `truthmark impact` +- `truthmark context` - verwaltete `AGENTS.md`-Workflow-Anweisungen -- generierte Skill-Flächen für Truth Structure, Truth Document, Truth Sync, Truth Realize und Truth Check für konfigurierte Agenten-Hosts +- generierte Truth Structure-, Truth Document-, Truth Sync-, Truth Realize- und Truth Check-Skill-Flächen für konfigurierte Agenten-Hosts - Branch-Scope-Metadaten -- Diagnostik für Konfiguration, Autorität, Routing, Entscheidungsstruktur, Frontmatter, Links und polyglotte Abdeckung +- Diagnostik für Konfiguration, Autorität, Routing, Entscheidungsstruktur, Frontmatter, Links, Freshness und polyglotte Abdeckung +- abgeleitete RepoIndex-, RouteMap-, ImpactSet- und ContextPack-Artefakte für schnellere lokale Prüfung, wenn die CLI verfügbar ist ## Dokumentation diff --git a/README.es.md b/README.es.md index 36c499d..6f7d857 100644 --- a/README.es.md +++ b/README.es.md @@ -150,34 +150,65 @@ Los repositorios existentes suelen necesitar una pasada de limpieza después de Truthmark es más fuerte en el camino por defecto, no como un conjunto de comandos manuales. El agente que actúa y el entorno anfitrión deciden si delegan o ejecutan el flujo instalado en línea. -Usa Truth Document cuando el comportamiento ya está implementado pero los documentos de verdad canónica faltan o son débiles. El agente lee implementación, pruebas, rutas y documentos existentes, escribe solo documentos de verdad y rutas, y no debe cambiar código funcional. Codex, Claude Code y los IDEs de Copilot compatibles pueden invocarlo con `/truthmark-document`; los hosts de estilo OpenCode pueden usar `/skill truthmark-document`. +### Comportamiento existente sin docs + +Usa esto cuando la implementación ya existe pero faltan o son débiles los documentos de verdad canónica: + +```text +el usuario identifica un comportamiento implementado o un endpoint de api +el usuario invoca explícitamente truth document +el agente lee implementación, pruebas, rutas y docs existentes +el agente solo escribe truth docs y rutas +revisar el diff de truth docs +``` + +Truth Document es manual y con prioridad de implementación: el código sirve como evidencia, los documentos de verdad se crean o reparan, y no se debe cambiar código funcional. Codex, Claude Code y los IDEs de Copilot compatibles pueden invocarlo con `/truthmark-document`; los hosts de estilo OpenCode pueden usar `/skill truthmark-document`. + +```text +/truthmark-document documenta el comportamiento implementado del timeout de sesión en docs/truth/authentication +``` + +### Cambios de código normales La mayoría de los usuarios no debería invocar Truth Sync directamente. Lo importante es que el flujo instalado del agente trate Truth Sync como una guarda de cierre cuando cambió código funcional. El flujo normal es: ```text el agente cambia código funcional se ejecutan las pruebas relevantes -el flujo instalado Truth Sync se ejecuta antes de que el agente termine -se revisa el diff de documentos de verdad si se produjo uno -se confirma o se entrega el trabajo +el flujo instalado truth sync se ejecuta antes de que el agente termine +revisar el diff de truth docs si se produjo uno +confirmar o entregar el trabajo ``` Truth Sync es code-first: el código lidera, los documentos de verdad siguen, y Truth Sync no debe reescribir código funcional. Su tarea principal es ejecutarse mediante el flujo instalado del agente como guarda de cierre cuando cambió código funcional. La invocación directa se usa sobre todo para depurar, forzar una sincronización temprana antes de entregar el trabajo o ejecutar el flujo de forma intencional. + Codex, Claude Code y los IDEs de Copilot compatibles pueden invocarlo con `/truthmark-sync`. Los hosts de estilo OpenCode pueden usar `/skill truthmark-sync`. + +```text +/truthmark-sync sincroniza ahora la verdad del repositorio antes de la entrega +``` + +### Cambios doc-first + Usa este flujo cuando una decisión de producto o arquitectura empieza en la documentación: ```text -el usuario edita los documentos de verdad -el usuario invoca explícitamente Truth Realize -el agente lee los documentos de verdad y el código relevante +el usuario edita truth docs +el usuario invoca explícitamente truth realize +el agente lee truth docs y el código relevante el agente actualiza solo el código se ejecutan las pruebas relevantes -se confirma o se entrega el trabajo +confirmar o entregar el trabajo ``` Truth Realize es manual y doc-first: los documentos de verdad lideran, el código sigue, y el agente no debe editar los documentos de verdad que está realizando. + Codex, Claude Code y los IDEs de Copilot compatibles pueden invocarlo con `/truthmark-realize`. Los hosts de estilo OpenCode pueden usar `/skill truthmark-realize`. +```text +/truthmark-realize realiza docs/truth/authentication/session-timeout.md como código +``` + ## Qué instala Truthmark mantiene pequeña y nativa del repositorio la superficie duradera de flujo de trabajo. Después de `truthmark init`, el propio repositorio lleva el enrutamiento, las reglas y las superficies instaladas, así que el equipo no depende solo de la configuración local de una persona. @@ -203,21 +234,42 @@ Las superficies generadas son administradas por Truthmark, incluyen un marcador ## Comandos -Truthmark V1 mantiene la CLI pequeña a propósito porque el flujo continuo debe vivir en las superficies instaladas del agente, no en una lista larga de comandos manuales de uso diario. En repositorios derivados, `truthmark config` crea el contrato de jerarquía confirmado en Git, `truthmark init` instala y refresca superficies de flujo de trabajo a partir de esa configuración revisada, y `truthmark check` valida los artefactos de verdad para auditorías manuales, CI o depuración. +Truthmark V1 mantiene la CLI pequeña a propósito porque el flujo continuo debe vivir en las superficies instaladas del agente, no en una lista larga de comandos manuales de uso diario. En repositorios derivados, `truthmark config` crea el contrato de jerarquía confirmado en Git, `truthmark init` instala y refresca superficies de flujo de trabajo a partir de esa configuración revisada, `truthmark check` valida los artefactos de verdad para auditorías manuales, CI o depuración, y los comandos de inteligencia del repositorio generan artefactos derivados de revisión cuando hay herramientas locales disponibles. ```bash truthmark config truthmark init truthmark check +truthmark index +truthmark impact --base main +truthmark context --workflow truth-sync --base main truthmark config --json truthmark check --json +truthmark index --json +truthmark impact --base main --json +truthmark context --workflow truth-sync --base main --json ``` `config` solo escribe `.truthmark/config.yml`, salvo que se use `--stdout`. + `init` requiere `.truthmark/config.yml` y luego instala o refresca los archivos locales de flujo de trabajo. + `check` valida configuración, autoridad, enrutamiento, documentos que contienen decisiones, frontmatter, enlaces internos, alcance de rama y diagnósticos de cobertura. + +`index` construye JSON de RepoIndex y RouteMap para el checkout activo. + +`impact --base ` mapea los archivos cambiados a los truth docs enrutados, rutas propietarias, pruebas cercanas y símbolos públicos. + +`context --workflow [--base ]` genera un ContextPack acotado para Truth Sync, Truth Document o Truth Realize. `--format markdown` lo renderiza en un formato legible para personas. + Truth Structure, Truth Document, Truth Sync, Truth Realize y Truth Check son flujos de trabajo instalados para agentes, no comandos CLI principales de uso diario. +Se ejecutan a través de las superficies configuradas del host agente, por ejemplo Codex/Claude/Copilot `/truthmark-*`, OpenCode `/skill truthmark-*` o Gemini `/truthmark:*`. + +```text +/truthmark-check audita el enrutamiento y la cobertura de verdad antes de la revisión +``` + ## Por qué existe La mayoría de los flujos de programación con IA optimizan la siguiente respuesta. Truthmark optimiza el siguiente traspaso. @@ -235,15 +287,20 @@ Asume que los equipos serios necesitan: ## Estado del proyecto Truthmark no es un servidor de memoria ni un servidor MCP. Es una práctica de repositorio empaquetada como un pequeño instalador CLI más superficies de flujo de trabajo nativas para agentes que convierten las reglas del flujo de IA en infraestructura del repositorio. + V1 actualmente ofrece: - `truthmark config` - `truthmark init` - `truthmark check` +- `truthmark index` +- `truthmark impact` +- `truthmark context` - instrucciones de flujo de trabajo administradas en `AGENTS.md` - superficies de skill generadas para Truth Structure, Truth Document, Truth Sync, Truth Realize y Truth Check en los anfitriones de agentes configurados - metadatos de alcance de rama - diagnósticos de configuración, autoridad, enrutamiento, estructura de decisiones, frontmatter, enlaces y cobertura políglota +- artefactos derivados de RepoIndex, RouteMap, ImpactSet y ContextPack para una revisión local más rápida cuando la CLI está disponible ## Documentación diff --git a/README.md b/README.md index 5b537a5..6de4e8e 100644 --- a/README.md +++ b/README.md @@ -240,14 +240,20 @@ Generated surfaces are managed by Truthmark, include a version marker, and may b ## Commands -Truthmark V1 intentionally keeps the CLI small because the ongoing workflow is meant to live in the installed agent surfaces, not in a long list of daily manual commands. In downstream repositories, `truthmark config` creates the committed hierarchy contract, `truthmark init` installs and refreshes workflow surfaces from that reviewed config, and `truthmark check` validates truth artifacts for manual audits, CI, or troubleshooting. +Truthmark V1 keeps the CLI focused because the ongoing workflow is meant to live in the installed agent surfaces, not in a long list of daily manual commands. In downstream repositories, `truthmark config` creates the committed hierarchy contract, `truthmark init` installs and refreshes workflow surfaces from that reviewed config, `truthmark check` validates truth artifacts for manual audits, CI, or troubleshooting, and the repository-intelligence commands generate derived review artifacts when local tooling is available. ```bash truthmark config truthmark init truthmark check +truthmark index +truthmark impact --base main +truthmark context --workflow truth-sync --base main truthmark config --json truthmark check --json +truthmark index --json +truthmark impact --base main --json +truthmark context --workflow truth-sync --base main --json ``` `config` writes only `.truthmark/config.yml` unless `--stdout` is used. @@ -256,6 +262,12 @@ truthmark check --json `check` validates configuration, authority, routing, decision-bearing docs, frontmatter, internal links, branch scope, and coverage diagnostics. +`index` builds RepoIndex and RouteMap JSON for the active checkout. + +`impact --base ` maps changed files to routed truth docs, owning routes, nearby tests, and public symbols. + +`context --workflow [--base ]` generates a bounded ContextPack for Truth Sync, Truth Document, or Truth Realize. `--format markdown` renders a human-readable pack. + Truth Structure, Truth Document, Truth Sync, Truth Realize, and Truth Check are installed agent workflows, not top-level daily CLI commands. They run through the configured agent host surfaces, for example Codex/Claude/Copilot `/truthmark-*`, OpenCode `/skill truthmark-*`, or Gemini `/truthmark:*`. @@ -288,10 +300,14 @@ V1 currently provides: - `truthmark config` - `truthmark init` - `truthmark check` +- `truthmark index` +- `truthmark impact` +- `truthmark context` - managed `AGENTS.md` workflow instructions - generated Truth Structure, Truth Document, Truth Sync, Truth Realize, and Truth Check skill surfaces for configured agent hosts - branch-scope metadata -- config, authority, routing, decision-structure, frontmatter, link, and polyglot coverage diagnostics +- config, authority, routing, decision-structure, frontmatter, link, freshness, and polyglot coverage diagnostics +- derived RepoIndex, RouteMap, ImpactSet, and ContextPack artifacts for faster local review when the CLI is available ## Documentation diff --git a/README.ru.md b/README.ru.md index 82f97d7..18a3d25 100644 --- a/README.ru.md +++ b/README.ru.md @@ -150,34 +150,65 @@ GEMINI.md Сильная сторона Truthmark — путь по умолчанию, а не набор ручных команд. Действующий агент и среда хоста сами решают, делегировать работу или выполнить установленный процесс на месте. -Используйте Truth Document, когда поведение уже реализовано, но канонические документы истины отсутствуют или слабы. Агент читает реализацию, тесты, маршруты и существующие документы, пишет только документы истины и маршруты и не должен менять функциональный код. Codex, Claude Code и поддерживаемые IDE Copilot могут вызывать его через `/truthmark-document`; хосты в стиле OpenCode могут использовать `/skill truthmark-document`. +### Существующее поведение без документации -Большинству пользователей не нужно вызывать Truth Sync напрямую. Главное, что установленный агентский процесс рассматривает Truth Sync как финальную защиту, когда менялся функциональный код. Нормальный путь выглядит так: +Используйте это, когда реализация уже есть, но канонические документы истины отсутствуют или слабы: + +```text +пользователь определяет реализованное поведение или api-эндпоинт +пользователь явно вызывает truth document +агент читает реализацию, тесты, маршрутизацию и существующие docs +агент пишет только truth docs и маршрутизацию +проверить diff truth docs +``` + +Truth Document — это ручной процесс с приоритетом реализации: код служит доказательством, документы истины создаются или исправляются, и функциональный код менять нельзя. Codex, Claude Code и поддерживаемые IDE Copilot могут вызывать его через `/truthmark-document`; хосты в стиле OpenCode могут использовать `/skill truthmark-document`. + +```text +/truthmark-document документирует реализованное поведение session timeout в docs/truth/authentication +``` + +### Обычные изменения кода + +Большинству пользователей не нужно напрямую вызывать Truth Sync. Главное, что установленный агентский процесс рассматривает Truth Sync как финальную защиту, когда менялся функциональный код. Нормальный путь выглядит так: ```text агент изменяет функциональный код запускаются релевантные тесты -установленный рабочий процесс Truth Sync выполняется до завершения работы агента -если был создан diff документов истины, он проверяется +установленный truth sync workflow запускается до завершения агента +если был создан diff truth docs, он проверяется работа коммитится или передается дальше ``` Truth Sync работает по принципу code-first: сначала идет код, затем документы истины, и Truth Sync не должен переписывать функциональный код. Его основная задача - выполняться через установленный агентский процесс как финальная защита, когда менялся функциональный код. Прямой вызов нужен в основном для отладки, ранней синхронизации перед передачей работы или намеренного запуска рабочего процесса. + Codex, Claude Code и поддерживаемые IDE Copilot могут вызывать его через `/truthmark-sync`. Хосты в стиле OpenCode могут использовать `/skill truthmark-sync`. + +```text +/truthmark-sync синхронизируй истину репозитория прямо сейчас перед передачей +``` + +### Doc-first изменения + Используйте этот путь, когда продуктовое или архитектурное решение начинается в документации: ```text -пользователь редактирует документы истины -пользователь явно вызывает Truth Realize -агент читает документы истины и связанный код +пользователь редактирует truth docs +пользователь явно вызывает truth realize +агент читает truth docs и связанный код агент обновляет только код запускаются релевантные тесты работа коммитится или передается дальше ``` -Truth Realize это ручной процесс по принципу doc-first: документы истины идут первыми, код следует за ними, и агент не должен редактировать документы истины, которые он реализует. +Truth Realize — это ручной doc-first процесс: документы истины идут первыми, код следует за ними, и агент не должен редактировать документы истины, которые он реализует. + Codex, Claude Code и поддерживаемые IDE Copilot могут вызывать его через `/truthmark-realize`. Хосты в стиле OpenCode могут использовать `/skill truthmark-realize`. +```text +/truthmark-realize реализуй docs/truth/authentication/session-timeout.md в код +``` + ## Что он устанавливает Truthmark держит постоянную рабочую поверхность маленькой и встроенной в репозиторий. После `truthmark init` сам репозиторий несет маршрутизацию, правила и установленные рабочие поверхности, поэтому команда не зависит только от локальной настройки одного человека. @@ -203,21 +234,42 @@ Truthmark держит постоянную рабочую поверхност ## Команды -Truthmark V1 намеренно держит CLI небольшим, потому что постоянный рабочий процесс должен жить в установленных агентских поверхностях, а не в длинном списке ежедневных ручных команд. В нижестоящих репозиториях `truthmark config` создает зафиксированный контракт иерархии, `truthmark init` устанавливает и обновляет рабочие поверхности на основе этой проверенной конфигурации, а `truthmark check` валидирует артефакты истины для ручных аудитов, CI или отладки. +Truthmark V1 намеренно держит CLI небольшим, потому что постоянный рабочий процесс должен жить в установленных агентских поверхностях, а не в длинном списке ежедневных ручных команд. В нижестоящих репозиториях `truthmark config` создает зафиксированный контракт иерархии, `truthmark init` устанавливает и обновляет рабочие поверхности на основе этой проверенной конфигурации, `truthmark check` валидирует артефакты истины для ручных аудитов, CI или отладки, а команды репозиторной аналитики создают производные артефакты для проверки, когда доступны локальные инструменты. ```bash truthmark config truthmark init truthmark check +truthmark index +truthmark impact --base main +truthmark context --workflow truth-sync --base main truthmark config --json truthmark check --json +truthmark index --json +truthmark impact --base main --json +truthmark context --workflow truth-sync --base main --json ``` `config` пишет только `.truthmark/config.yml`, если не используется `--stdout`. + `init` требует `.truthmark/config.yml`, а затем устанавливает или обновляет локальные файлы рабочих процессов. + `check` валидирует конфигурацию, полномочия, маршрутизацию, документы с решениями, frontmatter, внутренние ссылки, область действия ветки и диагностику покрытия. + +`index` строит JSON RepoIndex и RouteMap для активного checkout. + +`impact --base ` сопоставляет измененные файлы с routed truth docs, owning routes, nearby tests и public symbols. + +`context --workflow [--base ]` генерирует ограниченный ContextPack для Truth Sync, Truth Document или Truth Realize. `--format markdown` рендерит его в читаемый человеком вид. + Truth Structure, Truth Document, Truth Sync, Truth Realize и Truth Check это установленные агентские рабочие процессы, а не повседневные CLI-команды верхнего уровня. +Они запускаются через настроенные поверхности хоста агента, например Codex/Claude/Copilot `/truthmark-*`, OpenCode `/skill truthmark-*` или Gemini `/truthmark:*`. + +```text +/truthmark-check проверь маршрутизацию и покрытие truth перед review +``` + ## Зачем он существует Большинство ИИ-процессов для разработки оптимизируют следующий ответ. Truthmark оптимизирует следующую передачу работы. @@ -235,15 +287,20 @@ Truth Structure, Truth Document, Truth Sync, Truth Realize и Truth Check это ## Статус проекта Truthmark не является сервером памяти и не является MCP-сервером. Это репозиторная практика, упакованная как небольшой CLI-установщик и родные для агентов рабочие поверхности, которые превращают правила ИИ-процесса в инфраструктуру репозитория. + V1 сейчас предоставляет: - `truthmark config` - `truthmark init` - `truthmark check` +- `truthmark index` +- `truthmark impact` +- `truthmark context` - управляемые инструкции рабочих процессов в `AGENTS.md` - сгенерированные skill-поверхности Truth Structure, Truth Document, Truth Sync, Truth Realize и Truth Check для настроенных агентских хостов - метаданные области ветки - диагностика конфигурации, полномочий, маршрутизации, структуры решений, frontmatter, ссылок и полиглотного покрытия +- производные артефакты RepoIndex, RouteMap, ImpactSet и ContextPack для более быстрой локальной проверки, когда CLI доступен ## Документация diff --git a/README.zh.md b/README.zh.md index 066045f..5992f86 100644 --- a/README.zh.md +++ b/README.zh.md @@ -150,34 +150,65 @@ GEMINI.md Truthmark 最强的地方是默认路径,而不是一堆手动命令。由实际执行的代理和宿主环境决定是委托执行,还是内联运行已安装的工作流。 -当行为已经实现但规范事实文档缺失或质量较弱时,使用 Truth Document。代理会读取实现、测试、路由和现有文档,只写事实文档和路由,不能修改功能代码。Codex、Claude Code 和支持的 Copilot IDE 可以用 `/truthmark-document` 调用它;OpenCode 风格的宿主可以用 `/skill truthmark-document` 调用它。 +### 已实现但无文档的行为 + +当实现已经存在,但规范事实文档缺失或较弱时,使用这个流程: + +```text +用户识别一个已实现的行为或 API 端点 +用户显式调用 Truth Document +代理读取实现、测试、路由和现有文档 +代理只写 truth docs 和路由 +审查 truth-doc diff +``` + +Truth Document 是手动、implementation-first 的流程:代码作为证据,事实文档被创建或修复,且不能修改功能代码。Codex、Claude Code 和支持的 Copilot IDE 可以用 `/truthmark-document` 调用它;OpenCode 风格的宿主可以用 `/skill truthmark-document` 调用它。 + +```text +/truthmark-document 在 docs/truth/authentication 下记录已实现的会话超时行为 +``` + +### 常规代码变更 多数用户不需要直接调用 Truth Sync。关键在于,只要功能代码发生变化,已安装的代理工作流就会把 Truth Sync 当作收尾保护。正常路径是: ```text 代理修改功能代码 运行相关测试 -代理结束前运行已安装的 Truth Sync 工作流 -如果生成了事实文档 diff,就审查它 +已安装的 Truth Sync 工作流在代理结束前运行 +如果生成了 truth-doc diff,就审查它 提交或交接工作 ``` Truth Sync 是 code-first:代码在前,事实文档跟随,且 Truth Sync 不能重写功能代码。它的主要职责是在功能代码发生变化时,通过已安装的代理工作流充当收尾保护。直接调用主要用于排查问题、交接前提前同步,或有意运行这套工作流。 + Codex、Claude Code 和支持的 Copilot IDE 可以用 `/truthmark-sync` 调用它。OpenCode 风格的宿主可以用 `/skill truthmark-sync` 调用它。 + +```text +/truthmark-sync 现在同步仓库 truth,然后再交接 +``` + +### 文档优先变更 + 当产品或架构决策从文档开始时,使用这个流程: ```text -用户编辑事实文档 +用户编辑 truth docs 用户显式调用 Truth Realize -代理读取事实文档和相关代码 +代理读取 truth docs 和相关代码 代理只更新代码 运行相关测试 提交或交接工作 ``` Truth Realize 是手动、文档优先的流程:事实文档在前,代码跟随,代理不能编辑它正在实现的事实文档。 + Codex、Claude Code 和支持的 Copilot IDE 可以用 `/truthmark-realize` 调用它。OpenCode 风格的宿主可以用 `/skill truthmark-realize` 调用它。 +```text +/truthmark-realize 将 docs/truth/authentication/session-timeout.md 实现为代码 +``` + ## 它会安装什么 Truthmark 把持久化的工作流载体保持得很小,而且是仓库原生的。运行 `truthmark init` 之后,仓库本身就携带路由、规则和已安装的工作流载体,因此团队不再只依赖某个人的本地配置。 @@ -203,21 +234,42 @@ Truthmark 把持久化的工作流载体保持得很小,而且是仓库原生 ## 命令 -Truthmark V1 有意保持 CLI 很小,因为持续运行的工作流应该活在已安装的代理载体里,而不是一长串日常手动命令里。在下游仓库中,`truthmark config` 创建已提交的层级契约,`truthmark init` 根据这份已审查的配置安装和刷新工作流载体,`truthmark check` 则为人工审计、CI 或问题排查验证事实产物。 +Truthmark V1 有意保持 CLI 很小,因为持续运行的工作流应该活在已安装的代理载体里,而不是一长串日常手动命令里。在下游仓库中,`truthmark config` 创建已提交的层级契约,`truthmark init` 根据这份已审查的配置安装和刷新工作流载体,`truthmark check` 则为人工审计、CI 或问题排查验证事实产物,而仓库情报命令会在有本地工具时生成派生审查产物。 ```bash truthmark config truthmark init truthmark check +truthmark index +truthmark impact --base main +truthmark context --workflow truth-sync --base main truthmark config --json truthmark check --json +truthmark index --json +truthmark impact --base main --json +truthmark context --workflow truth-sync --base main --json ``` `config` 只写入 `.truthmark/config.yml`,除非使用 `--stdout`。 + `init` 需要 `.truthmark/config.yml`,然后安装或刷新本地工作流文件。 + `check` 验证配置、权限边界、路由、承载决策的文档、frontmatter、内部链接、分支范围和覆盖率诊断。 + +`index` 为当前 checkout 构建 RepoIndex 和 RouteMap JSON。 + +`impact --base ` 会把变更文件映射到已路由的 truth docs、所属路由、附近测试和 public symbols。 + +`context --workflow [--base ]` 会为 Truth Sync、Truth Document 或 Truth Realize 生成一个受限的 ContextPack。`--format markdown` 会把它渲染成可读文本。 + Truth Structure、Truth Document、Truth Sync、Truth Realize 和 Truth Check 是已安装的代理工作流,不是日常使用的顶层 CLI 命令。 +它们通过已配置的代理宿主表面运行,例如 Codex/Claude/Copilot 的 `/truthmark-*`、OpenCode 的 `/skill truthmark-*`,或者 Gemini 的 `/truthmark:*`。 + +```text +/truthmark-check 在 review 前审计路由和 truth 覆盖 +``` + ## 它为什么存在 大多数 AI 编码工作流优化的是下一次回答。Truthmark 优化的是下一次交接。 @@ -235,15 +287,20 @@ Truth Structure、Truth Document、Truth Sync、Truth Realize 和 Truth Check ## 项目状态 Truthmark 不是记忆服务器,也不是 MCP 服务器。它是一套仓库实践,被打包成一个小型 CLI 安装器和代理原生的工作流载体,用来把 AI 工作流规则变成仓库基础设施。 + V1 目前提供: - `truthmark config` - `truthmark init` - `truthmark check` +- `truthmark index` +- `truthmark impact` +- `truthmark context` - 受管的 `AGENTS.md` 工作流说明 - 为已配置代理宿主生成的 Truth Structure、Truth Document、Truth Sync、Truth Realize 和 Truth Check 技能载体 - 分支范围元数据 - 配置、权限边界、路由、决策结构、frontmatter、链接和多语言覆盖率诊断 +- RepoIndex、RouteMap、ImpactSet 和 ContextPack 派生产物,可在 CLI 可用时加快本地检查 ## 文档 diff --git a/changes/.template.md b/changes/.template.md new file mode 100644 index 0000000..a010c2f --- /dev/null +++ b/changes/.template.md @@ -0,0 +1,15 @@ +# Short Change Title + +Version action: none|patch|minor|major + +## PR Summary + +- Concise maintainer-facing summary. + +## Release Note + +- None; internal-only change. + +## Verification + +- Not run yet. diff --git a/changes/2026-05-16-internal-change-notes-flow.md b/changes/2026-05-16-internal-change-notes-flow.md new file mode 100644 index 0000000..f6d3917 --- /dev/null +++ b/changes/2026-05-16-internal-change-notes-flow.md @@ -0,0 +1,16 @@ +# Internal Change Notes Flow + +Version action: none + +## PR Summary + +- Added an internal `changes/` note flow so agents can produce reusable PR and release source text. +- Linked package version decisions to change notes without making the flow a user-facing product feature. + +## Release Note + +- None; internal-only repository maintenance standard. + +## Verification + +- `npm run dev -- check` diff --git a/changes/2026-05-16-repository-intelligence-cli.md b/changes/2026-05-16-repository-intelligence-cli.md new file mode 100644 index 0000000..c9e4d07 --- /dev/null +++ b/changes/2026-05-16-repository-intelligence-cli.md @@ -0,0 +1,16 @@ +# Repository Intelligence CLI + +Version action: minor + +## PR Summary + +- Added public `truthmark index`, `truthmark impact`, and `truthmark context` commands with RepoIndex, RouteMap, ImpactSet, and ContextPack JSON contracts. +- Added freshness diagnostics and workflow-facing repository-intelligence guidance while preserving direct checkout inspection as the source of authority. + +## Release Note + +- Added repository-intelligence commands for derived index, impact, and context-pack output: `truthmark index`, `truthmark impact --base `, and `truthmark context --workflow `. + +## Verification + +- `npm test -- tests/context-pack/build.test.ts tests/repo-index/build.test.ts tests/impact/build.test.ts tests/cli/index-impact-context.test.ts` diff --git a/docs/README.md b/docs/README.md index 6966d0f..5db6a29 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,6 +65,8 @@ Canonical docs should include frontmatter and keep these fields current: - When repository-wide agent policy changes, update [docs/ai/repo-rules.md](ai/repo-rules.md). - When code-to-doc routing changes, update [docs/truthmark/areas.md](truthmark/areas.md) in the same change. +- When package versions or release/version policy change, apply [docs/standards/versioning.md](standards/versioning.md) before accepting the version. +- When PR or release source text is needed, write a `changes/` note using [docs/standards/change-notes.md](standards/change-notes.md). - When `truthmark init` or scaffolded files change, update the relevant truth or architecture doc, not only [README.md](../README.md). - When `truthmark check` changes what it validates or how it reports diagnostics, update both the current truth doc and the contract doc. - When major product, onboarding, install, command, positioning, or workflow behavior changes, review the root [README.md](../README.md) and update it if the human entry point would otherwise be stale. diff --git a/docs/ai/agent-onboarding.md b/docs/ai/agent-onboarding.md index 05be4a2..796085a 100644 --- a/docs/ai/agent-onboarding.md +++ b/docs/ai/agent-onboarding.md @@ -49,6 +49,21 @@ Read: 3. [docs/standards/maintaining-repository-truth.md](../standards/maintaining-repository-truth.md) 4. [docs/ai/agent-skills-workflow-review.md](agent-skills-workflow-review.md) +### Package version changes + +Read: + +1. [docs/standards/versioning.md](../standards/versioning.md) +2. [docs/standards/change-notes.md](../standards/change-notes.md) +3. [docs/standards/testing-and-verification.md](../standards/testing-and-verification.md) + +### PR or release text + +Read: + +1. [docs/standards/change-notes.md](../standards/change-notes.md) +2. [docs/standards/versioning.md](../standards/versioning.md), only when package version changes are in scope + ### Documentation structure or policy changes Read: @@ -64,6 +79,8 @@ Do: - treat [docs/ai/repo-rules.md](repo-rules.md) as the primary authority for repository-wide rules - route code changes to the nearest maintained architecture, contract, and truth docs - update [docs/truthmark/areas.md](../truthmark/areas.md) when canonical routing changes +- apply [docs/standards/versioning.md](../standards/versioning.md) before changing or accepting a package version +- write `changes/` notes from [docs/standards/change-notes.md](../standards/change-notes.md) when PR or release text is needed - preserve the generated Truthmark block in [AGENTS.md](../../AGENTS.md) unless the template behavior itself is changing - keep non-canonical planning notes separate from current-state docs diff --git a/docs/ai/repo-rules.md b/docs/ai/repo-rules.md index e22a05e..1ee4133 100644 --- a/docs/ai/repo-rules.md +++ b/docs/ai/repo-rules.md @@ -28,7 +28,7 @@ Code is the implementation. On code/doc conflict, inspect code, decide whether c ## Product Boundary -Truthmark has three CLI commands: `config`, `init`, and `check`. Truth Structure, Truth Document, Truth Sync, Truth Realize, and Truth Check are installed workflow surfaces, not CLI commands. +Truthmark public CLI commands are `config`, `init`, `check`, `index`, `impact`, and `context`. Truth Structure, Truth Document, Truth Sync, Truth Realize, and Truth Check are installed workflow surfaces, not CLI commands. Agents inspect the active checkout directly. There is no daemon, database, remote service, or V1 MCP server. diff --git a/docs/standards/change-notes.md b/docs/standards/change-notes.md new file mode 100644 index 0000000..27dbf4d --- /dev/null +++ b/docs/standards/change-notes.md @@ -0,0 +1,76 @@ +--- +status: active +doc_type: standard +last_reviewed: 2026-05-16 +source_of_truth: + - versioning.md + - pre-completion-checklist.md +--- + +# Change Notes + +## Trigger + +Use this standard when a change needs PR text, release text, or a package version decision. + +Required triggers: + +- `package.json` version changes +- root package entries in `package-lock.json` change because the package version changed +- the user asks for PR, release, changelog, or handoff text +- the working change alters published package behavior + +Optional trigger: + +- internal-only maintenance that would benefit from a reusable PR summary + +## Folder + +Write change notes under `changes/`. + +Use one file per cohesive working change: + +```text +changes/YYYY-MM-DD-short-slug.md +``` + +Use dates from the current local session date. Keep slugs short, lowercase, and grep-friendly. + +## Required Shape + +Each change note must use this structure: + +```markdown +# Short Change Title + +Version action: none|patch|minor|major + +## PR Summary + +- Concise maintainer-facing summary. + +## Release Note + +- User-facing release text, or `None; internal-only change.` + +## Verification + +- Command run, or skipped check with reason. +``` + +## Rules + +- `Version action` must match [versioning.md](versioning.md). +- If `Version action` is `patch`, `minor`, or `major`, the package version must change in the same working change. +- If the package version changes, a matching change note is required because Truthmark releases whenever the package version changes. +- Internal-only repository standards may use `Version action: none`. +- Release notes describe published package behavior, not private repo maintenance. +- Keep notes compact; they are source material for PR and release descriptions, not canonical product truth. + +## Agent Output + +When reporting change-note work, state only: + +- change note path +- version action +- whether release text is present or intentionally `None` diff --git a/docs/standards/pre-completion-checklist.md b/docs/standards/pre-completion-checklist.md index d3505fb..10c83fc 100644 --- a/docs/standards/pre-completion-checklist.md +++ b/docs/standards/pre-completion-checklist.md @@ -5,6 +5,8 @@ last_reviewed: 2026-05-13 source_of_truth: - testing-and-verification.md - documentation-governance.md + - versioning.md + - change-notes.md --- # Pre-Completion Checklist @@ -20,6 +22,8 @@ Use this checklist before declaring Truthmark work complete. - If this was a major product, onboarding, install, command, positioning, or workflow change, did you review the root [README.md](../../README.md) and update stale user-facing claims, examples, or command sequences? - If the root README changed materially, did the localized README variants change in the same working change? If not, stop unless you have an explicit repo-authorized reason they intentionally remain different, and state that reason. A passing verification command does not waive this gate. - If canonical routing changed, did [docs/truthmark/areas.md](../truthmark/areas.md) change too? +- If the change is PR-worthy, release-worthy, or changes the package version, did you add or update a `changes/` note using [docs/standards/change-notes.md](change-notes.md)? +- If any package version or release/version policy changed, did you apply [docs/standards/versioning.md](versioning.md) before accepting the version? - If [AGENTS.md](../../AGENTS.md) changed, did manual edits stay outside the managed Truthmark block? - Did you run the narrowest meaningful verification command from [docs/standards/testing-and-verification.md](testing-and-verification.md)? - If a normally expected verification step was skipped, did you state the reason explicitly? diff --git a/docs/standards/testing-and-verification.md b/docs/standards/testing-and-verification.md index 62d1d8b..6a7f149 100644 --- a/docs/standards/testing-and-verification.md +++ b/docs/standards/testing-and-verification.md @@ -5,6 +5,7 @@ last_reviewed: 2026-05-13 source_of_truth: - ../../package.json - ../truth/contracts.md + - versioning.md --- # Testing And Verification @@ -44,6 +45,7 @@ If a linked `truthmark` binary points at this checkout's `dist/main.js`, `truthm - Run `npm run dev -- check` when canonical docs, authority order, or areas routing changes. - Run `npm run check` before closing out broader code changes unless a narrower command is the only relevant one. - Run `npm run release:check` before publishing or handing off release-sensitive packaging changes. +- Run `npm run dev -- init --json`, inspect generated version-marker diffs, and then run `npm run dev -- check` when the package version changes. ## Documentation-Only Changes diff --git a/docs/standards/versioning.md b/docs/standards/versioning.md new file mode 100644 index 0000000..201bae0 --- /dev/null +++ b/docs/standards/versioning.md @@ -0,0 +1,83 @@ +--- +status: active +doc_type: standard +last_reviewed: 2026-05-16 +source_of_truth: + - ../../package.json + - ../../package-lock.json + - change-notes.md + - https://semver.org/ +--- + +# Versioning + +## Trigger + +Use this standard only when a task changes or asks whether to change: + +- `package.json` version +- root package entries in `package-lock.json` +- release/version policy +- a `changes/` note with `Version action: patch`, `minor`, or `major` + +Do not load this standard for ordinary docs, workflow text, generated-surface, or code changes unless a package version decision is in scope. + +## Goal + +Choose Truthmark's own package version using Semantic Versioning 2.0.0. Normal committed versions use `MAJOR.MINOR.PATCH`. + +`package.json` is the maintained version source. `package-lock.json` follows it. Generated version markers follow the package version only after `truthmark init` is rerun. + +This is an internal repository maintenance standard. It is not a user-facing feature, installed workflow feature, or generated workflow capability. + +## First Gate + +If the change does not alter published package behavior, do not bump the package version. + +No bump examples: + +- internal standards for this repository +- agent routing guidance for maintainers +- documentation corrections with no published behavior change +- tests, refactors, formatting, or cleanup with no published behavior change + +## Decision Table + +| Change | Version action | +| --- | --- | +| Backward-incompatible public API or shipped workflow change | `MAJOR + 1`, reset `MINOR` and `PATCH` to `0` | +| Backward-compatible public API addition, generated-surface capability, shipped workflow capability, deprecation, or substantial user-visible improvement | `MINOR + 1`, reset `PATCH` to `0` | +| Backward-compatible bug fix, diagnostic correction, packaging fix, or published documentation correction | `PATCH + 1` | +| Internal-only maintenance with no published package behavior change | no version change | + +Do not use prerelease or build metadata in the committed package version unless the release task explicitly asks for it. + +## Public API For Bump Decisions + +Treat these as published package behavior: + +- CLI command names, options, exit behavior, result envelopes, and diagnostics contracts +- `.truthmark/config.yml` schema and hierarchy behavior +- generated instruction blocks, skill metadata, prompt files, and version markers produced by the package +- installed workflow boundaries, trigger contracts, report shapes, and completion gates as shipped package behavior +- runtime compatibility and npm package contents + +## Version Change Procedure + +When changing a version number: + +1. Decide the bump class before editing the version and state the rationale in the handoff, PR, or release note. +2. Create or update the matching `changes/` note from [change-notes.md](change-notes.md). +3. Update `package.json` and the root package entries in `package-lock.json` together. +4. Rerun `truthmark init` only when the package version actually changes, then inspect generated version-marker diffs. +5. Run the focused verification required by [testing-and-verification.md](testing-and-verification.md). For release-sensitive package version changes, `npm run release:check` is the default final gate. + +## Agent Output + +When reporting a package version decision, state only: + +- chosen version action +- one-line SemVer rationale +- files changed or intentionally left unchanged +- matching change note path when a version changes +- verification run or explicitly skipped diff --git a/docs/truth/check-diagnostics.md b/docs/truth/check-diagnostics.md index 530f764..54ea867 100644 --- a/docs/truth/check-diagnostics.md +++ b/docs/truth/check-diagnostics.md @@ -8,6 +8,9 @@ source_of_truth: - ../../src/checks/authority.ts - ../../src/checks/areas.ts - ../../src/checks/branch-scope.ts + - ../../src/freshness/check.ts + - ../../src/impact/build.ts + - ../../src/evidence/validate.ts - ../../src/checks/frontmatter.ts - ../../src/checks/links.ts - ../../src/markdown/discovery.ts @@ -29,12 +32,15 @@ The command: 2. computes branch-scope metadata 3. loads `.truthmark/config.yml` 4. runs authority, area, decision-structure, frontmatter, internal-link, generated-surface, and coverage diagnostics when config is valid -5. returns a human summary or the shared JSON envelope +5. when `--base ` is supplied, builds an ImpactSet and adds freshness diagnostics for changed code without route ownership, stale evidence, invalid base comparisons, and changed public API without docs sync +6. returns a human summary or the shared JSON envelope There is no supported `--workflow` helper mode. Agent workflows inspect the checkout directly and may run `truthmark check` only as optional validation. The command reports repository truth health for the active checkout. It does not prepare mandatory workflow context, choose verification commands, or decide whether a coding task can finish. +`truthmark check` without `--base` keeps the existing checkout-health behavior and does not compute ImpactSet. `truthmark check --base ` adds branch-impact freshness diagnostics and includes `data.impactSet` in JSON output. + Topology repair remains an installed workflow responsibility. `truthmark check` may expose routing or coverage symptoms, but AI agents must be able to perform Truth Structure directly from committed config, route files, docs, and implementation when the Truthmark binary is unavailable. ## Validation Passes @@ -160,12 +166,29 @@ Current severity behavior: - configured generated surface content stale: `review` - generated Truthmark version marker differs from the current package version: `review` +### Freshness + +Freshness checks run only when `--base ` is supplied. + +Current severity behavior: + +- changed functional code with no route owner: `review` +- changed public API with no affected truth document: `review` +- changed public API with affected truth docs that were not changed in the impact set: `review` +- invalid base ref or failed base comparison: `error` +- deleted `source_of_truth` or evidence reference: `error` +- `source_of_truth` glob reference with no matching files: `error` +- evidence reference outside the repository root: `error` +- missing evidence symbol, invalid evidence span, or stale evidence hash: `error` +- evidence line spans are validated even when the evidence block does not include a content hash + ## Result Shape - human output reports the number of `error` and `review` diagnostics - JSON output returns the shared command envelope - JSON output includes `data.branchScope` - JSON output includes `data.truthVisibility` +- JSON output includes `data.impactSet` only when `--base ` is supplied - JSON output does not include workflow payloads Branch scope identifies the active checkout: diff --git a/docs/truth/contracts.md b/docs/truth/contracts.md index e120c12..9b5686f 100644 --- a/docs/truth/contracts.md +++ b/docs/truth/contracts.md @@ -2,27 +2,29 @@ status: active doc_type: contract truth_kind: contract -last_reviewed: 2026-05-14 +last_reviewed: 2026-05-16 source_of_truth: - ../../src/config/schema.ts - ../../src/checks/check.ts - ../../src/templates/init-files.ts + - ../../src/templates/generated-surfaces.ts - ../../src/init/init.ts - ../../src/output/diagnostic.ts - ../../src/output/render.ts + - ../../src/cli/handlers.ts --- # Contracts ## Scope -This document defines the current machine-facing contracts exposed by Truthmark: the config file shape and the CLI result envelope. +This document defines the current machine-facing contracts exposed by Truthmark: the config file shape, route metadata, repository-intelligence artifacts, and the CLI result envelope. ## Contract Surface - The committed `.truthmark/config.yml` schema and defaults. - Route metadata under `docs/truthmark/areas.md` and delegated child route files. -- The JSON result envelope emitted by `truthmark config`, `truthmark init`, and `truthmark check`. +- The JSON result envelope emitted by `truthmark config`, `truthmark init`, `truthmark check`, `truthmark index`, `truthmark impact`, and `truthmark context`. ## Inputs @@ -104,7 +106,7 @@ Current shape: Diagnostic fields: -- `category`: one of `config`, `authority`, `frontmatter`, `links`, `area-index`, `coverage`, `truth-sync`, `realization`, `doc-structure`, or `generated-surface` +- `category`: one of `config`, `authority`, `frontmatter`, `links`, `area-index`, `coverage`, `truth-sync`, `realization`, `doc-structure`, `generated-surface`, `repo-index`, `impact`, `freshness`, or `context-pack` - `severity`: one of `info`, `action`, `review`, or `error` - `message`: human-readable detail - `file`: optional repository-relative file path @@ -113,6 +115,14 @@ Diagnostic fields: Human-rendered output is intended for people. JSON output is the machine-facing contract. +`truthmark index --json` returns `data.repoIndex` with `schemaVersion: repo-index/v0` and `data.routeMap` with `schemaVersion: route-map/v0`. + +`truthmark impact --base --json` returns `data.impactSet` with `schemaVersion: impact-set/v0`. + +`truthmark context --workflow [--base ] --json` returns `data.contextPack` with `schemaVersion: context-pack/v0`. `--workflow` accepts `truth-sync`, `truth-document`, and `truth-realize`. `--format` accepts `json` or `markdown`; unsupported formats return a `context-pack` error diagnostic. `--format markdown` renders a deterministic Markdown ContextPack for human review, and `--json --format markdown` includes that Markdown under `data.markdown`. + +RepoIndex, RouteMap, ImpactSet, and ContextPack are derived from the active checkout. They do not override route files, source files, truth docs, or installed workflow write boundaries. + ## Compatibility Rules - `version` remains `1` in the committed config contract. @@ -243,7 +253,7 @@ For normal branches, `identity` is branch name plus HEAD SHA. For detached check - The committed config file owns the documentation hierarchy contract, while route files own domain-to-doc mappings. - `truthmark config` and `truthmark init` are separate contracts so repositories can review hierarchy before workflow installation. - Active decisions stay in the canonical doc they govern instead of in separate timestamped decision logs. Date active decisions inline when added or changed. -- The V1 user-facing CLI surface is limited to `config`, `init`, and `check`; workflow verbs such as `sync`, `realize`, `structure`, `audit`, `packet`, `review`, `scan`, `doctor`, `build`, and `context` are not top-level commands. +- The V1 user-facing CLI surface is `config`, `init`, `check`, `index`, `impact`, and `context`; workflow verbs such as `sync`, `realize`, `structure`, `audit`, `packet`, `review`, `scan`, `doctor`, and `build` are not top-level commands. - `gemini-cli` installs both hierarchical `GEMINI.md` context and project-scoped `.gemini/commands/truthmark/*.toml` custom commands so Gemini users get the same explicit workflow entrypoints without adding top-level CLI verbs. - Decision (2026-05-14): Truth Realize is manually invoked through installed workflow surfaces and is not controlled by `realization.enabled` or any other config key. diff --git a/docs/truth/init-and-scaffold.md b/docs/truth/init-and-scaffold.md index 76b8b6c..ff7f5f5 100644 --- a/docs/truth/init-and-scaffold.md +++ b/docs/truth/init-and-scaffold.md @@ -155,8 +155,8 @@ Important current defaults: Current init JSON reporting uses: -- `truth-sync` for the managed `AGENTS.md` block and generated Truth Structure, Truth Document, Truth Sync, and Truth Check skill assets -- `realization` for generated Truth Realize skill assets +- `truth-sync` for the managed `AGENTS.md` block and generated Truth Structure, Truth Document, Truth Sync, and Truth Check assets +- `realization` for generated Truth Realize assets - `authority` for [docs/truthmark/areas.md](../truthmark/areas.md) - `config` for the remaining scaffolded files diff --git a/docs/truth/release/automation.md b/docs/truth/release/automation.md index 550f43b..7e93244 100644 --- a/docs/truth/release/automation.md +++ b/docs/truth/release/automation.md @@ -35,6 +35,8 @@ This doc covers the committed GitHub Actions workflows under `.github/workflows/ Release automation runs through the committed GitHub Actions workflows under `.github/workflows/`. The `CI` workflow verifies repository changes, and the `Publish` workflow revalidates release state before publishing to npm. +The repository also ships `examples/github-actions/truthmark-impact.yml` as a consumer example. It is not a release workflow for this repository. The example shows a non-blocking PR mode that comments with `truthmark impact --base` and `truthmark check --base` results, uploads the JSON reports, and a blocking mode controlled by `TRUTHMARK_BLOCKING`. + ## Steps - The `CI` workflow runs on pushes to `main` and on every pull request. @@ -47,6 +49,7 @@ Release automation runs through the committed GitHub Actions workflows under `.g - Failed verification or release-check steps stop the current job and prevent later publish steps from running. - Publishing occurs only for the GitHub release event path; branch pushes and pull requests do not publish. - GitHub Actions reruns remain host-managed; Truthmark owns the committed workflow definitions, not GitHub's execution controls. +- The example ImpactSet workflow is intentionally opt-in and consumer-copied; it does not run unless a repository installs it. ## Outputs diff --git a/docs/truth/repository/context-pack.md b/docs/truth/repository/context-pack.md new file mode 100644 index 0000000..5d5d19e --- /dev/null +++ b/docs/truth/repository/context-pack.md @@ -0,0 +1,51 @@ +--- +status: active +doc_type: behavior +truth_kind: behavior +last_reviewed: 2026-05-16 +source_of_truth: + - ../../../src/context-pack/build.ts + - ../../../src/context-pack/render.ts + - ../../../src/impact/build.ts +--- + +# ContextPack + +## Scope + +This document owns ContextPack v0 behavior for Truth Sync, Truth Document, and Truth Realize workflows. + +## Current Behavior + +`truthmark context --workflow [--base ] --json` generates a bounded context artifact for a workflow. `--format markdown` renders a deterministic human-readable pack, and `--json --format markdown` includes the rendered Markdown in `data.markdown`. ContextPack rejects unsupported `--format` values with a `context-pack` error diagnostic. ContextPack includes route ownership, affected truth docs, selected source files, related tests, warnings, and allowed write paths. + +ContextPack output includes `schemaVersion: context-pack/v0`. It is generated from the active checkout and, when a base ref is supplied, ImpactSet. + +## Core Rules + +- ContextPack is optional generated context and does not replace direct checkout inspection. +- ContextPack write paths restate workflow boundaries; they do not create new permissions. +- Truth Sync and Truth Document write paths include the route index and selected truth docs. Truth Realize write paths include the matched route code surfaces because Realize may write functional code but not truth docs or routing. +- Truth Realize without `--base` cannot infer matched code surfaces, so selected truth docs, source files, and `allowedWritePaths` are empty and ContextPack emits a review warning instead of widening to every route. +- Source files include changed files from ImpactSet when a base ref is supplied and `source_of_truth` references from selected truth docs. Glob references are expanded against the checkout. +- ContextPack-only text is not evidence. Generated docs must cite checkout files, route files, truth docs, tests, schemas, or explicit evidence blocks. +- If ContextPack conflicts with the current checkout, the checkout wins. + +## Runtime Dependency Boundary + +ContextPack requires the Truthmark CLI or an equivalent local runner. If unavailable, agents must follow the installed workflow manually by reading route files, truth docs, source files, and tests directly. Completion reports must say ContextPack was not generated. + +## Product Decisions + +- Decision (2026-05-16): ContextPack v0 is a bounded review artifact, not memory and not a source of authority. +- Decision (2026-05-16): No-CLI workflow execution remains supported, but with weaker automation and explicit reporting. + +## Rationale + +ContextPack makes agent context auditable without making hidden retrieval or stale generated artifacts authoritative. Keeping it derived prevents a fast path from changing ownership or write behavior. + +## Primary Code Files + +- `src/context-pack/build.ts` +- `src/context-pack/render.ts` +- `src/impact/build.ts` diff --git a/docs/truth/repository/impact-set.md b/docs/truth/repository/impact-set.md new file mode 100644 index 0000000..d2f6797 --- /dev/null +++ b/docs/truth/repository/impact-set.md @@ -0,0 +1,55 @@ +--- +status: active +doc_type: behavior +truth_kind: behavior +last_reviewed: 2026-05-16 +source_of_truth: + - ../../../src/impact/build.ts + - ../../../src/impact/git-diff.ts + - ../../../src/repo-index/build.ts +--- + +# ImpactSet + +## Scope + +This document owns ImpactSet v0 behavior. ImpactSet maps Git changes to Truthmark routes, truth docs, owning areas, related tests, and public symbol changes. + +## Current Behavior + +`truthmark impact --base --json` compares the active checkout to the supplied base ref and returns `schemaVersion: impact-set/v0`. The command combines Git diff data with RepoIndex and RouteMap data. + +ImpactSet reports changed files, affected routes, affected truth docs, affected tests, changed public symbols, and diagnostics. It includes staged, unstaged, and untracked worktree changes so local agent work can be evaluated before commit. + +## Core Rules + +- Changed functional code is mapped through `Code surface` entries in Truthmark route files. +- Changed routed truth docs are mapped back to their owning routes and included in `affectedTruthDocs`. +- Renamed files preserve `previousPath`, map both old and new paths to route ownership, and report moved exports as removed from the old path and added at the new path. +- Changed test files are reported as affected tests instead of missing truth-route diagnostics. +- Related tests are selected from direct imports, basename hints, and package-level test path conventions. +- Public symbol changes are computed from JavaScript/TypeScript exports in v0. +- Missing or invalid base refs produce an `impact` error diagnostic instead of silently returning an empty comparison. +- Changed public symbols produce review diagnostics when no affected truth doc exists or when affected truth docs exist but were not changed in the impact set. +- ImpactSet is derived. It does not grant write permission and does not replace route ownership. + +## Runtime Dependency Boundary + +ImpactSet requires the Truthmark CLI or an equivalent local runner. If unavailable, agents must inspect Git changes and route ownership directly. The workflow may proceed manually, but completion reports must say ImpactSet was not generated. + +If an ImpactSet conflicts with the current checkout, agents must trust the checkout and rerun or ignore the artifact. + +## Product Decisions + +- Decision (2026-05-16): ImpactSet v0 uses Git plus route ownership as the review boundary instead of a background cache. +- Decision (2026-05-16): Public API impact starts with JavaScript and TypeScript exports because this repository's first implementation stack is TypeScript. + +## Rationale + +ImpactSet gives Truth Sync and CI a stable, reviewable way to explain what code changed and which truth surfaces are affected without making a model decide ownership. + +## Primary Code Files + +- `src/impact/build.ts` +- `src/impact/git-diff.ts` +- `src/repo-index/build.ts` diff --git a/docs/truth/repository/repo-index.md b/docs/truth/repository/repo-index.md new file mode 100644 index 0000000..2bee0a4 --- /dev/null +++ b/docs/truth/repository/repo-index.md @@ -0,0 +1,55 @@ +--- +status: active +doc_type: behavior +truth_kind: behavior +last_reviewed: 2026-05-16 +source_of_truth: + - ../../../src/repo-index/build.ts + - ../../../src/repo-index/file-tree.ts + - ../../../src/repo-index/route-map.ts + - ../../../src/repo-index/types.ts +--- + +# RepoIndex + +## Scope + +This document owns RepoIndex v0 and RouteMap v0 behavior. RepoIndex describes the current checkout's files, docs, packages, tests, JavaScript/TypeScript imports and exports, public symbols, and Truthmark route ownership. + +## Current Behavior + +`truthmark index --json` builds a deterministic repository index from the active checkout. The command reads local files and Git metadata only. It does not start a daemon, call a remote service, use a model, or write generated artifacts by default. + +RepoIndex output includes `schemaVersion: repo-index/v0`. RouteMap output includes `schemaVersion: route-map/v0` and is derived from `.truthmark/config.yml`, `docs/truthmark/areas.md`, and `docs/truthmark/areas/**/*.md`. + +## Core Rules + +- Paths are repository-relative POSIX paths. +- Arrays are sorted lexicographically unless source order is part of the contract. +- File discovery honors Git ignore rules through `git ls-files --exclude-standard` and then applies Truthmark config ignores, so ignored local artifacts are not indexed. +- Symbol extraction v0 covers JavaScript and TypeScript source files. +- Managed agent Markdown surfaces such as `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and Copilot instructions are classified as generated files rather than ordinary docs. +- Route ownership comes from Truthmark area files, not from package structure or import graphs. +- RepoIndex and RouteMap are derived artifacts. They speed up routing and review, but they do not override source files, route files, or truth docs. + +## Runtime Dependency Boundary + +RepoIndex requires the Truthmark CLI or an equivalent local runner to compute. If the CLI is unavailable, agents must inspect `.truthmark/config.yml`, route files, changed source files, and routed truth docs directly. Workflows may proceed manually, but completion reports must say RepoIndex and RouteMap were not generated. + +If a RepoIndex or RouteMap artifact conflicts with the current checkout, the checkout wins. Agents must rerun the CLI when available or ignore the stale artifact when it cannot be regenerated. + +## Product Decisions + +- Decision (2026-05-16): RepoIndex v0 is a local deterministic acceleration layer, not a source of truth. +- Decision (2026-05-16): RouteMap v0 is aligned to `docs/truthmark/areas.md` instead of introducing a parallel ownership schema. + +## Rationale + +Keeping repository intelligence derived preserves Truthmark's branch-local review boundary. Teams can use fast machine-readable context when the CLI is available without making installed workflows unusable in constrained agent environments. + +## Primary Code Files + +- `src/repo-index/build.ts` +- `src/repo-index/file-tree.ts` +- `src/repo-index/route-map.ts` +- `src/repo-index/types.ts` diff --git a/docs/truth/workflows/shared-gates.md b/docs/truth/workflows/shared-gates.md index a763350..0632d6f 100644 --- a/docs/truth/workflows/shared-gates.md +++ b/docs/truth/workflows/shared-gates.md @@ -63,6 +63,14 @@ Truth Check uses an audit-shaped gate: findings and suggested fixes need evidenc Truth Sync completed reports have a deterministic structured parser for Markdown reports with `Evidence checked` entries containing `Claim`, `Evidence`, and `Result` fields. +## Repository Intelligence Boundary + +RepoIndex, RouteMap, ImpactSet, and ContextPack are derived acceleration artifacts. They may guide routing, context selection, evidence review, and verification planning, but they do not own truth, route authority, or write permissions. + +When the Truthmark CLI is unavailable, workflows must inspect `.truthmark/config.yml`, `docs/truthmark/areas.md`, child area files, changed source files, and routed truth docs directly. The workflow may proceed manually, but the completion report must say that RepoIndex, ImpactSet, ContextPack, and freshness diagnostics were not generated. + +If a repository-intelligence artifact conflicts with the current checkout, the checkout wins. Agents rerun the CLI when available or ignore the stale artifact when it cannot be regenerated. ContextPack-only content is not evidence; generated truth docs must cite checkout files, route files, truth docs, tests, schemas, or explicit evidence blocks. + ## Shape Repair Gate Shape repair is in-place cleanup inside an already-bounded truth owner. It covers missing template sections, stale evidence conflicts, cross-section updates within one owner, or wrong frontmatter/source/headings. diff --git a/docs/truth/workflows/truth-document.md b/docs/truth/workflows/truth-document.md index eedf5b5..fb8c1f2 100644 --- a/docs/truth/workflows/truth-document.md +++ b/docs/truth/workflows/truth-document.md @@ -44,6 +44,8 @@ When ownership is bounded, Truth Document creates or updates leaf truth docs, ke When Truth Document restructures a bounded truth doc or runs Structure first, it inventories Product Decisions and Rationale before editing. Existing entries must be preserved in or moved to their bounded owner docs, narrowed or removed only with checkout evidence, or blocked for manual review when ownership is unclear. +ContextPack may be used to gather bounded source context when available. It does not replace checkout inspection, does not create write permission, and cannot be cited as evidence unless it points to real checkout files, tests, route files, truth docs, schemas, or explicit evidence blocks. If ContextPack is unavailable, Truth Document proceeds manually and reports that repository-intelligence artifacts were not generated. + Completed reports include `Implementation reviewed`, `Ownership reviewed`, `Structure required` when applicable, `Truth docs created`, `Truth docs updated`, `Truth docs restructured`, `Routing updated`, `Evidence checked`, and `Notes`. ## Product Decisions diff --git a/docs/truth/workflows/truth-realize.md b/docs/truth/workflows/truth-realize.md index b8f7b1d..bf710c5 100644 --- a/docs/truth/workflows/truth-realize.md +++ b/docs/truth/workflows/truth-realize.md @@ -42,6 +42,8 @@ When source truth is bounded and current, Truth Realize implements only the boun Completion reports include `Truth docs used`, `Code updated`, and `Verification`. +ContextPack may be used to collect bounded implementation context when available. It does not replace checkout inspection, does not grant write permission outside the workflow boundary, and cannot override source truth docs or current code. If ContextPack is unavailable, Truth Realize proceeds manually and reports that repository-intelligence artifacts were not generated. + ## Product Decisions - Decision (2026-05-15): Truth Realize remains available only through explicit user invocation and is always generated for configured platforms instead of being gated by a config toggle. diff --git a/docs/truth/workflows/truth-sync.md b/docs/truth/workflows/truth-sync.md index 8e0c2f1..9f1fab7 100644 --- a/docs/truth/workflows/truth-sync.md +++ b/docs/truth/workflows/truth-sync.md @@ -47,6 +47,8 @@ When Truth Sync restructures a bounded truth doc or runs Structure inline, it in Truth Sync updates architecture docs in the same sync when changed code alters architecture-level structure or ownership. +ContextPack may be used to accelerate Truth Sync when available. It does not replace checkout inspection, does not create write permission, and cannot be cited as evidence unless it points to real checkout files, tests, route files, truth docs, schemas, or explicit evidence blocks. If ContextPack or ImpactSet is unavailable, Truth Sync proceeds manually and reports that repository-intelligence artifacts were not generated. + Completed reports include `Changed code reviewed`, `Ownership reviewed`, `Structure required` when applicable, `Truth docs updated`, `Truth docs split` when Structure is run inline, `Evidence checked`, and `Notes`. Skipped reports include `Reason`. Blocked reports include `Reason`, `Files requiring manual review`, and `Next action`. Current skip reasons are: diff --git a/docs/truthmark/areas.md b/docs/truthmark/areas.md index 80a24b3..a9ca80b 100644 --- a/docs/truthmark/areas.md +++ b/docs/truthmark/areas.md @@ -92,7 +92,24 @@ Area files: Code surface: - .github/workflows/** +- src/templates/github-action.ts Update truth when: - CI verification steps or triggers change - release publishing prerequisites or publish steps change + +## Repository Intelligence + +Area files: +- docs/truthmark/areas/repository-intelligence.md + +Code surface: +- src/repo-index/** +- src/impact/** +- src/evidence/** +- src/freshness/** +- src/context-pack/** + +Update truth when: +- repository indexing, route-map, impact analysis, evidence validation, freshness checks, or context-pack behavior changes +- `truthmark index`, `truthmark impact`, or `truthmark context` result shapes change diff --git a/docs/truthmark/areas/release-automation.md b/docs/truthmark/areas/release-automation.md index 1a22c1a..afd43a9 100644 --- a/docs/truthmark/areas/release-automation.md +++ b/docs/truthmark/areas/release-automation.md @@ -21,7 +21,9 @@ truth_documents: Code surface: - .github/workflows/** +- src/templates/github-action.ts Update truth when: - CI verification steps or triggers change - release publishing prerequisites or publish steps change +- GitHub Action examples or action template rendering changes diff --git a/docs/truthmark/areas/repository-intelligence.md b/docs/truthmark/areas/repository-intelligence.md new file mode 100644 index 0000000..4113742 --- /dev/null +++ b/docs/truthmark/areas/repository-intelligence.md @@ -0,0 +1,35 @@ +--- +status: active +doc_type: routing +last_reviewed: 2026-05-16 +source_of_truth: + - ../areas.md + - ../../README.md + - ../../ai/repo-rules.md +--- + +# Repository Intelligence Areas + +## Repository Intelligence + +Truth documents: +```yaml +truth_documents: + - path: docs/truth/repository/repo-index.md + kind: behavior + - path: docs/truth/repository/impact-set.md + kind: behavior + - path: docs/truth/repository/context-pack.md + kind: behavior +``` + +Code surface: +- src/repo-index/** +- src/impact/** +- src/evidence/** +- src/freshness/** +- src/context-pack/** + +Update truth when: +- RepoIndex, RouteMap, ImpactSet, evidence validation, freshness diagnostics, or ContextPack behavior changes +- repository-intelligence command output changes diff --git a/examples/github-actions/truthmark-impact.yml b/examples/github-actions/truthmark-impact.yml new file mode 100644 index 0000000..06d5594 --- /dev/null +++ b/examples/github-actions/truthmark-impact.yml @@ -0,0 +1,61 @@ +name: Truthmark Impact + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + truthmark-impact: + runs-on: ubuntu-latest + env: + TRUTHMARK_BLOCKING: "false" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - name: Generate ImpactSet + run: | + npx truthmark impact --base origin/${{ github.base_ref }} --json > impactset.json + npx truthmark check --base origin/${{ github.base_ref }} --json > truthmark-check.json + - name: Upload Truthmark reports + uses: actions/upload-artifact@v4 + with: + name: truthmark-impact + path: | + impactset.json + truthmark-check.json + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require("node:fs"); + const impact = JSON.parse(fs.readFileSync("impactset.json", "utf8")); + const check = JSON.parse(fs.readFileSync("truthmark-check.json", "utf8")); + const docs = impact.data.impactSet.affectedTruthDocs; + const body = [ + "## Truthmark Impact", + "", + impact.summary, + check.summary, + "", + "Affected truth docs:", + ...(docs.length === 0 ? ["- none"] : docs.map((path) => `- ${path}`)), + ].join("\n"); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + - name: Enforce blocking mode + if: env.TRUTHMARK_BLOCKING == 'true' + run: | + node -e "const check=require('./truthmark-check.json'); const bad=check.diagnostics.filter((d)=>d.severity==='error'||d.category==='freshness'); if (bad.length) { console.error(JSON.stringify(bad, null, 2)); process.exit(1); }" diff --git a/package-lock.json b/package-lock.json index 3a73cf1..aea40fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "truthmark", - "version": "1.2.4", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "truthmark", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "dependencies": { "ajv": "^8.17.1", @@ -16,6 +16,7 @@ "gray-matter": "^4.0.3", "micromatch": "^4.0.8", "remark-parse": "^11.0.0", + "typescript": "^5.9.3", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "yaml": "^2.8.1" @@ -31,7 +32,6 @@ "prettier": "^3.8.3", "tsup": "^8.5.0", "tsx": "^4.20.6", - "typescript": "^5.9.3", "typescript-eslint": "^8.59.2", "vitest": "^4.0.8" }, @@ -5105,7 +5105,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 4b5b821..6484c6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "truthmark", - "version": "1.2.4", + "version": "1.3.0", "description": "Git-native, branch-scoped truth workflow installer for local AI coding agents.", "license": "MIT", "type": "module", @@ -50,6 +50,7 @@ "gray-matter": "^4.0.3", "micromatch": "^4.0.8", "remark-parse": "^11.0.0", + "typescript": "^5.9.3", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "yaml": "^2.8.1" @@ -62,7 +63,6 @@ "prettier": "^3.8.3", "tsup": "^8.5.0", "tsx": "^4.20.6", - "typescript": "^5.9.3", "typescript-eslint": "^8.59.2", "vitest": "^4.0.8" } diff --git a/research/2026-05-14-repoagent-ai-doc-gen-truthmark-comparison.md b/research/2026-05-14-repoagent-ai-doc-gen-truthmark-comparison.md index be0896b..aa2dc9d 100644 --- a/research/2026-05-14-repoagent-ai-doc-gen-truthmark-comparison.md +++ b/research/2026-05-14-repoagent-ai-doc-gen-truthmark-comparison.md @@ -29,7 +29,7 @@ The highest-leverage direction is to combine RepoAgent's persistent impact model | Project | Core user value | Main problem solved | Intended workflow | Repository assumptions | Optimized output | Developer experience | | --- | --- | --- | --- | --- | --- | --- | | RepoAgent | Keep detailed internal code documentation synchronized with a Python repository at object granularity. | Internal docs rot because nobody updates function and class explanations after code changes. | Parse the repo into objects, extract references, generate docs per object, persist metadata, then rerun incrementally on later code changes. | Git repo, Python source, willingness to keep `.project_doc_record` and generated markdown in-repo, staged-change oriented workflow. | Per-file and per-symbol markdown docs under `markdown_docs/`, backed by persistent metadata. | Automation-first. It fits a pre-commit flow and tries to make documentation maintenance feel like a side effect of normal git usage. | -| ai-doc-gen | Produce usable high-level documentation and AI onboarding artifacts quickly with minimal repository-specific engineering. | Repositories often lack a current README, AI context files, and analysis notes that help both humans and models understand the codebase. | Run `analyze` to emit `.ai/docs/*.md`, then run `generate readme` or `generate ai-rules`, optionally through cron automation. | Repo is inspectable through directory listing plus targeted file reads; LLM can infer structure from code cues; generated analysis docs are acceptable intermediates. | AI-facing analysis markdown, README.md, CLAUDE.md, AGENTS.md, Cursor rules. | Operator-driven and approachable. The user runs explicit commands and gets high-level artifacts without needing a persistent semantic model. | +| ai-doc-gen | Produce usable high-level documentation and AI onboarding artifacts quickly with minimal repository-specific engineering. | Repositories often lack a current README, AI context files, and analysis notes that help both humans and models understand the codebase. | Run `analyze` to emit `.ai/docs/*.md`, then run `generate readme` or `generate ai-rules`, optionally through cron automation. | Repo is inspectable through directory listing plus targeted file reads; LLM can infer structure from code cues; generated analysis docs are acceptable intermediates. | AI-facing analysis markdown, README.md, CLAUDE.md, AGENTS.md, and editor assistant rules. | Operator-driven and approachable. The user runs explicit commands and gets high-level artifacts without needing a persistent semantic model. | | TruthMark | Keep canonical repository truth current, bounded, and writable by agents without losing ownership or trust. | Docs drift because teams lack a stable answer to "what doc owns this behavior" and agents lack hard boundaries on what they may update. | Configure docs topology, install workflow surfaces, let agents inspect the checkout directly, then use Truth Sync / Document / Structure / Check to maintain truth. | Teams want branch-local markdown as canonical truth; agents can inspect code directly; routing metadata can map behavior to bounded truth docs. | Canonical truth docs, route files, installed agent workflow surfaces, diagnostics. | Governance-first and agent-native. It optimizes for correct placement and safe maintenance of truth, not for automatic narrative generation. | ### Bottom line @@ -86,7 +86,7 @@ ai-doc-gen's central pattern is a staged analysis pipeline with reusable AI-faci - Internal representation: the main intermediate representation is markdown analysis artifacts in `.ai/docs/` rather than a graph structure. This is the defining architectural choice. - Analyzer decomposition: `AnalyzerAgent.run` spins up independent agents for structure, dependencies, data flow, request flow, and API analysis. Each agent has a role-specific system prompt and a fixed markdown output contract in `analyzer.yaml`. - Context building: `_render_prompt` injects repo path plus full repo structure string. The LLM can then call `Read-File` and `List-Files-Tool` during the run. -- Documentation generation: `DocumenterAgent` reads the available `.ai/docs/*.md` files and optionally an existing README, then writes `README.md` using a Pydantic output model. `AIRulesGeneratorAgent` reuses the same pattern for `CLAUDE.md`, `AGENTS.md`, and Cursor rules. +- Documentation generation: `DocumenterAgent` reads the available `.ai/docs/*.md` files and optionally an existing README, then writes `README.md` using a Pydantic output model. `AIRulesGeneratorAgent` reuses the same pattern for `CLAUDE.md`, `AGENTS.md`, and editor assistant rules. - Output shaping and constraints: prompts live in YAML, output schemas use Pydantic models for several generators, and there are simple postchecks such as required analysis-file presence and `AGENTS.md` line-count warnings. - Update behavior: it can reuse existing generated files as references, but it does not compute semantic impact or stale regions inside the repo. The `cronjob` path only decides whether a repository should be reanalyzed and then opens an MR. @@ -228,7 +228,7 @@ The outputs are practical: - `.ai/docs/*.md` for machine-readable repository knowledge - `README.md` for humans -- `CLAUDE.md`, `AGENTS.md`, Cursor rules for AI assistant onboarding +- `CLAUDE.md`, `AGENTS.md`, and editor assistant rules for AI assistant onboarding That is a strong user experience because the outputs line up with actual developer jobs. diff --git a/research/truthmark_competitive_research_report_2026-05-15.md b/research/truthmark_competitive_research_report_2026-05-15.md new file mode 100644 index 0000000..7eb1d5c --- /dev/null +++ b/research/truthmark_competitive_research_report_2026-05-15.md @@ -0,0 +1,345 @@ +# TruthMark Competitive Research Report + +Status judgments are as of **2026-05-15**. This report treats TruthMark as a Git-native truth/workflow product, not as a generic documentation generator. + +## 1. Executive summary + +TruthMark’s real competitive category is **repository truth governance for AI-assisted development**. Its current baseline is clear: it installs branch-scoped, Git-native truth workflows; routes docs through `docs/truthmark/areas.md`; emits agent-facing surfaces such as `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`; and stays local-first with no daemon, database, remote service, or MCP dependency by default. + +The market is moving quickly toward adjacent but not identical products. Continuous-docs vendors such as **DeepDocs, DocSync, Dosu, Red Hat Code-to-Docs, Swimm, Driver, and Mintlify** are converging on “detect code/docs drift → open or suggest a documentation PR → keep a human in the loop.” Several are more mature than TruthMark on immediate code-to-doc synchronization. DeepDocs claims every commit triggers a docs scan and PR, DocSync claims merge-driven updates plus exportable Markdown and MCP, Dosu’s Generate Docs uses code diffs, PR conversations, issues, and tickets, and Red Hat’s Code-to-Docs uses PR comment commands plus a two-stage semantic index committed under `.doc-index`. + +Codebase-wiki and code-understanding systems are not direct substitutes, but they are the biggest perception threat. **Google Code Wiki, Cognition DeepWiki/Devin Wiki, Sourcegraph Cody, Greptile, Qodo, CodeSee, Unblocked, and Pieces** are training buyers to expect repo-wide explanations, diagrams, Q&A, code links, context retrieval, and multi-repo understanding. Google Code Wiki and DeepWiki generate continuously updated wikis with code links and diagrams; Greptile and Qodo build codebase-aware PR review/context systems; Unblocked adds tickets, Slack, PRs, docs, and decisions as context; Pieces captures local workflow memory and exposes it through MCP. + +The agent-instruction ecosystem strongly validates TruthMark’s multi-agent workflow-surface hypothesis. `AGENTS.md` is an emerging shared format, Codex reads `AGENTS.md` and supports Skills, Claude Code loads `CLAUDE.md`, GitHub Copilot supports `.github/copilot-instructions.md`, `.github/instructions/*.instructions.md`, `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`, Gemini CLI supports `GEMINI.md` plus configurable context file names, and opencode supports `AGENTS.md` plus Skills. + +The strongest defensible TruthMark position is not “better generated docs.” It is **branch-scoped, Git-reviewable, source-grounded truth with explicit route ownership and bounded agent write surfaces**. The existing research note already points in the right direction: RepoIndex, ImpactSet, ContextPack, source evidence spans, and validation checks should become core product architecture, not optional research ideas. + +The main strategic risk is that TruthMark remains a workflow shell while competitors ship better semantic indexing, impact detection, PR update automation, and source-grounded citations. The main opportunity is that most competitors still trap “truth” in SaaS indexes, generated hosted wikis, chat context, local memory, or docs sites. TruthMark can own the narrower but more durable wedge: **canonical truth committed to the repo, scoped to the branch, reviewable by humans, and consumable by every coding agent.** + +## 2. Competitive landscape map + +| Category | Representative products | What they are optimizing for | Threat to TruthMark | TruthMark lesson | +|---|---|---:|---:|---| +| Continuous documentation sync | DeepDocs, DocSync, Dosu, Red Hat Code-to-Docs, Swimm, Driver, Mintlify | Detect drift and update docs from code changes, often through PRs | High | Copy PR-based sync, drift scoring, and semantic impact detection; avoid becoming generic docs automation. | +| Generated codebase wiki / Q&A | Google Code Wiki, DeepWiki, OpenDeepWiki, Davia, CodeSee | Repo understanding, diagrams, onboarding, exploration | Medium-high | Treat generated maps as derived artifacts, not canonical truth. | +| Codebase context / review intelligence | Sourcegraph Cody, Greptile, Qodo, CodeRabbit, Unblocked, Pieces, Bloop | Context retrieval, code review, agent grounding, institutional memory | Medium | Build RepoIndex + ImpactSet; do not compete head-on as an IDE/code-review platform. | +| Agent instruction and workflow surfaces | AGENTS.md, Codex, Claude Code, Copilot, Gemini CLI, opencode, Packmind, `.agents` CLI | Govern agent behavior through repo instructions, skills, commands, hooks, memory | High opportunity | Make TruthMark a compiler for multi-agent truth workflows. | +| Docs-as-code and API docs platforms | Mintlify, GitBook, ReadMe, Fern, Stainless, Speakeasy, Docusaurus, MkDocs, TypeDoc, JSDoc, OpenAPI/Swagger/Redocly | Publish docs, API references, SDK docs, MCP/API surfaces | Low-medium | Integrate with them as output targets; do not become a docs-site platform. | +| Open-source implementation references | AgenticCodebase, Context7, Davia, OpenDeepWiki, Redocly CLI, TypeDoc JSON, OpenAPI Generator | Semantic graphs, source spans, MCP, generated artifacts, deterministic validation | Medium opportunity | Study implementation patterns; keep canonical TruthMark layer smaller and auditable. | + +## 3. Product landscape matrix + +Abbreviations: **SoT** = source of truth. **Write** = how the tool changes artifacts. **Depth** = repository understanding depth. **TM action** = copy, adapt, avoid, or position against. + +| # | Product | Category / status / target | Inputs → outputs | SoT / freshness / write | Trust, depth, integration, privacy/adoption | TM action / threat | +|---:|---|---|---|---|---|---| +| 1 | TruthMark | Baseline; active OSS repo; AI-assisted dev teams | Code/docs/routes → `docs/truth`, `areas.md`, agent files, checks | Repo-committed branch truth; manual/check-driven; local file edits | Git review, route ownership, local-first, multi-agent surfaces; current V1 is workflow/config heavy | Own this category; direct baseline | +| 2 | DeepDocs | Continuous docs sync; status unclear because site active but Marketplace says deprecated; GitHub teams | Commits/code/docs → docs PRs | Code-first; every commit scan; PR branch | Claims full-repo scan, doc-code mapping, PR logs; Marketplace showed 245 installs and deprecated app | Copy PR UX; avoid distribution ambiguity; direct | +| 3 | DocSync | Docs sync + MCP; active-looking SaaS; dev teams | Code/repos → Markdown docs, MCP Q&A | Generated/exportable docs; merge/background; writes docs back | Claims code analyzed not stored, generated docs stored/exportable, private GitHub/Bitbucket, MCP for Claude and other clients | Adapt export+MCP; direct | +| 4 | Dosu Generate Docs | Docs automation; active SaaS/GitHub app; OSS/product teams | Code diffs, PR chats, issues, tickets → draft docs | Code + conversation signals; PR/draft; human merge | Human review; Marketplace install signal for Dosu app; freshness CI uses deterministic and LLM signals | Copy input-signal breadth; direct | +| 5 | Red Hat Code-to-Docs | OSS GitHub Action; active article April 2026; platform/docs teams | PR diffs, Jira/Confluence/Google Docs, comments → gap report/docs PR | Code/spec-first; PR comment trigger; PR | Human control, `.doc-index`, semantic folder/file summaries, multi-LLM support | Copy commands + committed index; direct implementation reference | +| 6 | Swimm | Continuous internal docs; mature commercial; engineering teams | Code snippets/docs/PRs → `.swm` docs, CI checks, auto-sync | Repo-stored docs; PR/CI; auto-sync commits/comments | Patented Auto-sync, docs in Git, CI, air-gap/SOC2/ISO claims | Copy code-coupled docs; direct | +| 7 | Driver AI | Docs in repo; active; codebase/API docs teams | Commits/code → `driver_docs` Markdown PRs | Repo folder; every commit; PR/MR | PR review, closes stale pending PRs, public API docs for C headers | Copy high-frequency PR handling; direct | +| 8 | Mintlify | Docs-as-code + agent updates; active commercial; devrel/API teams | Code changes/docs repo → docs PR/branch | Docs-site/repo hybrid; GitHub Actions/API; PR or branch | Agent API workflows, GitHub App, enterprise plan for automation | Integrate output target; direct-adjacent | +| 9 | GitBook | Hosted docs + Git sync + agent; active; docs teams | Git/visual editor/agent → docs changes/change requests | Docs-first with Git sync; branch/CR; merge | Two-way GitHub/GitLab sync, branch-like change requests, agent-assisted reviews | Adapt review workflow; adjacent | +| 10 | Doclific | Local internal docs; active OSS-looking; small teams | Repo/code snippets/AI assistant → local rich docs, ERDs, whiteboards | Repo-local docs; manual/AI; local edits | Local-first, code snippets move with Git changes and require review on content change; Claude skills | Copy smart snippets; adjacent | +| 11 | Google Code Wiki | Generated code wiki; public preview; repo readers | Full codebase/changes → wiki, diagrams, chat | Generated wiki; continuous regen; hosted/cloud preview | Hyperlinked file/definition citations, architecture/class/sequence diagrams; local Gemini CLI extension planned | Position against as non-canonical wiki; adjacent | +| 12 | Cognition DeepWiki / Devin Wiki | Generated wiki/Q&A; active; open-source/private repo users | GitHub repos → wiki, diagrams, Ask Devin | Generated hosted wiki; background indexing; mostly read-only | Auto-indexes repos, source links, `.devin/wiki.json` steering, public repos free, private through Devin | Use as perception benchmark; adjacent | +| 13 | Sourcegraph Cody | Code assistant/context; active enterprise; large codebases | Local/remote repos → chat, edits, completions | SaaS/Sourcegraph index + IDE context; background/search; local edits | Uses Sourcegraph Search API, local and remote context, symbols/usage patterns; VS Code/JetBrains/Visual Studio/web | Do not compete as assistant; integrate/serve context; adjacent | +| 14 | Greptile | AI code review/context graph; active; teams | PRs/code/comments/rules → PR reviews/tests | SaaS index; PR-triggered; comments/suggestions | Builds graph over files/functions/classes/vars, calls/imports/deps/usages; 9,000+ teams claimed | Copy graph/impact patterns; adjacent threat | +| 15 | Bloop | Code search/Q&A; archived repo; individual/team users | Local/GitHub repos → search, Q&A, patches | Local/GitHub index; manual; generated patches | Rust search, tree-sitter navigation, symbol search; GitHub org marks `bloop` public archive with 9.5k stars | Study parser/search only; low | +| 16 | CodeSee | Visual code maps; active-looking; engineering teams | Repo/dependencies/PRs → maps, knowledge views, PR maps | SaaS/visual graph; code-change watch; mostly read/alerts | Auto-generated/updated maps, PR impact maps, dependency views, alerts before merge, AI Q&A | Adapt graph visualization; adjacent | +| 17 | Pieces | Local memory/context; active; individual developers | Clipboard, screen, audio, apps, IDE → local memory, MCP context | Local memory; continuous capture; read-only context | On-device processing, searchable knowledge graph, MCP integrations with IDEs/AI tools | Avoid hidden personal memory as canonical truth; adjacent | +| 18 | Unblocked | Engineering context engine; active; teams/agents | Code, docs, tickets, Slack, PRs → sourced answers/context via MCP/CLI/API/Slack | SaaS context graph; evolving index; read/context | Source deconfliction, permission awareness, MCP for Claude/Copilot; sources in answers | Adapt institution-context model; adjacent threat | +| 19 | Qodo | Code review/governance/context; active; enterprise | PRs, repo history, standards, tickets → PR feedback/rules | SaaS/on-prem; PR trigger; comments | Multi-agent review, rule system, context engine, multi-repo structural/semantic/embedding index, on-prem option | Copy rule enforcement framing; adjacent | +| 20 | CodeRabbit | Code review/planning; active; teams | PRs, Jira/Linear, Slack, IDE/CLI → reviews, plans, PRs | SaaS/Git; PR/CLI/Slack; comments/PRs | Knowledge base, multi-repo analysis, MCP, CLI/IDE reviews; free and paid tiers | Avoid code-review war; adjacent | +| 21 | Davia | Visual editable repo wiki; active OSS alpha; agent users | Local repo + coding agent → local interactive docs/whiteboards | Local docs + optional cloud push; manual/agent; files | Designed for Copilot, Claude, Augment, and similar coding agents; 1.6k stars, no releases at capture | Copy local visual artifacts; adjacent | +| 22 | OpenDeepWiki / DeepWiki-open | OSS wiki/MCP; active OSS; self-hosters | Git repos/ZIP/local files → knowledge base, README/docs, Mermaid, MCP | Generated KB; manual/background; hosted/self-hosted | Multi-repo, multi-language, MCP, custom models; inspired by DeepWiki | Study MCP/wiki architecture; adjacent | +| 23 | Context7 | Docs context/MCP; active OSS; agent users | Library docs/version queries → current docs in prompt/MCP | External docs index; prompt-time; read-only | CLI + Skills or MCP; agent setup for Claude/opencode; solves stale API-doc context | Adapt “skill or MCP, not MCP-only”; adjacent | +| 24 | AGENTS.md | Agent instruction standard; active; repo maintainers | Repo instructions → agent behavior | Repo file; hierarchical/manual; read-only | Open format, “README for agents,” over 60k OSS projects claimed, compatible with many agents | Compile to/from it; high opportunity | +| 25 | OpenAI Codex | Agent host; active; developers | `AGENTS.md`, Skills, plugins, repo → Codex tasks | Repo/global instructions; session-load; local/cloud agent edits | Reads AGENTS.md by scope; Skills package workflows/scripts/resources; progressive disclosure | First-class target; high | +| 26 | Claude Code | Agent host; active; developers/teams | `CLAUDE.md`, memory, hooks, skills → agent behavior | Repo/user/org memory; loaded each session; local edits | Team-shared CLAUDE.md via version control; memory is context, not strict enforcement | Target, but do not rely on compliance; high | +| 27 | GitHub Copilot instructions | Agent/platform; active; GitHub teams | `.github/copilot-instructions.md`, path rules, AGENTS/CLAUDE/GEMINI → Copilot Chat/agent | Repo instructions; task-load; edits/PRs | Supports repo-wide, path-specific, and AGENTS.md-style instruction files | First-class target; high | +| 28 | Gemini CLI | Agent host; active; developers | `GEMINI.md`, imports, configurable file names → CLI context | Repo/workspace/global context; JIT scan; local edits | Supports hierarchy, `/memory`, imports, and configurable context file names including AGENTS.md/CONTEXT.md/GEMINI.md | First-class target; high | +| 31 | opencode | Agent host; active OSS/commercial; developers | AGENTS.md, skills, instruction files → agent behavior | Repo/global; scoped; local edits | Skills discovered in `.opencode/skills`, `.claude/skills`, `.agents/skills`; AGENTS.md preferred | First-class target; high | +| 32 | `.agents` CLI | Multi-agent config compiler; active small OSS; agent-heavy teams | `.agents/` source → tool-specific instructions/MCP/skills | Repo source of truth; sync/watch; writes configs | One `.agents` source, syncs AGENTS/CLAUDE/GEMINI/Copilot configs; 63 stars in captured page | Adapt compiler pattern; adjacent | +| 33 | Packmind | Context engineering/governance; active-looking; teams | Repo scan + agent onboarding → standards, commands, skills | Central playbook; CLI/self-host/cloud; drafts | Centralizes playbook and distributes tool-specific instruction files; onboard creates draft standards/commands | Position against: governance but not truth docs; adjacent | +| 34 | ReadMe MCP | API docs → MCP; active; API teams | API docs/spec → MCP server/config | Docs/API spec; generated MCP; read/execute controls | Generates MCP from API docs, supports many clients, per-route disabling, search layer | Adapt per-route permissions; low-medium | +| 35 | Fern | API docs/SDK docs; active; API teams | OpenAPI + config → API refs, docs, AI chat | Spec/docs repo; Git workflow; generated docs | Compared with Stainless: Git workflow, AI chat, WYSIWYG/Slack AI writer | Integrate output; low | +| 36 | Stainless | API docs/SDK/MCP; active; API companies | OpenAPI + SDK/docs config → Astro repo, docs, SDK refs, MCP | Full docs repo; Git/CI; generated PRs/artifacts | Full docs site/prose/API/SDK refs, docs-as-code, Astro repo, MCP generation | Avoid API-platform drift; low | +| 37 | Speakeasy / Gram | API SDK/docs/MCP; active; API teams | OpenAPI → SDK samples, API refs, MCP tools | Spec-first; generated; artifacts | Auto-synced SDK code samples and MCP operation tooling; Gram for managed hosting/OAuth per comparison source | Integrate specs; low | +| 38 | Docusaurus / MkDocs | Static docs frameworks; active OSS; docs teams | Markdown/MDX/YAML → static site | Docs-as-code; manual/build; generated site | Docusaurus has MDX/versioning/search; MkDocs uses Markdown/YAML/plugins/static HTML | Output targets only; not competitor | +| 39 | TypeDoc / JSDoc | API doc generators; active OSS; JS/TS teams | Source comments/exports → HTML/JSON docs | Code-comment-first; build; generated artifacts | TypeDoc emits HTML/JSON from TS exports; JSDoc scans JS comments to generate docs | Use as deterministic source extraction; low | +| 40 | OpenAPI / Swagger / Redocly / Stoplight | API spec/docs tooling; active OSS/commercial; API teams | OpenAPI specs → docs, clients, lint reports | Spec-first; CI/build; generated artifacts | OpenAPI Generator creates clients/servers/docs; Swagger UI visualizes specs; Redocly lints/validates; Stoplight Elements embeds API docs | Copy lint/CI model; not direct | + +## 4. Deep dives on the most relevant products + +### 4.1 DeepDocs + +DeepDocs is the closest “AI keeps repo docs fresh” competitor by positioning. Its official workflow is simple: install the GitHub app, choose a repo/branch, scan the repo, and open a documentation PR; future commits trigger docs PRs automatically. It claims full-repo scanning, code-to-doc mapping, style-preserving edits, logs, monorepo/separate-docs-repo support, and support for Docusaurus, Mintlify, ReadTheDocs, and MkDocs. + +The caveat is status risk: the GitHub Marketplace listing returned “This app has been deprecated,” while the official website still presents an active product. That means it should be treated as **unclear status**, not as a fully reliable current competitor. + +TruthMark should copy the **commit-triggered docs PR** and **code/doc mapping** UX. It should avoid DeepDocs’ apparent ambiguity around distribution status. The positioning gap is that DeepDocs updates docs; TruthMark can govern canonical repo truth, routes, ownership, and agent behavior. + +### 4.2 DocSync + +DocSync is a direct docs-sync competitor with a stronger agent-context story than most. It claims to detect merged code changes, update affected docs automatically, generate architecture/API/module docs, export Markdown, and expose an MCP server compatible with Claude Code and other MCP clients. It also claims private GitHub/Bitbucket support, code analyzed but not stored, and generated docs written back to the repository. + +This is a serious adjacent/direct threat because it combines docs freshness, repository export, and agent consumption. TruthMark should not ignore MCP, but should keep MCP optional. The defensible contrast is: DocSync’s center appears to be generated documentation and SaaS processing; TruthMark’s center should be **branch-scoped committed truth plus reviewable governance**. + +### 4.3 Dosu Generate Docs and freshness scoring + +Dosu’s Generate Docs uses code diffs, PR conversations, issues, and tickets as inputs. Its workflow is “read repo changes → verify whether docs need updates → create a draft if needed → humans review and merge.” + +Dosu’s docs freshness work is also strategically relevant. Its post describes a PR-time freshness-scoring pipeline using deterministic signals such as Git age delta, frontmatter TTL, symbol-level drift, and an LLM pass. It also notes that Git-only docs are invisible to teams whose docs live elsewhere, which is a useful warning for TruthMark if it wants to support mixed docs locations later. + +TruthMark should adapt the scoring pattern into `truthmark check`: route coverage, stale evidence spans, changed-symbol/doc impact, and missing owner diagnostics. It should avoid adopting a pure “score” unless the score decomposes into actionable, reviewable findings. + +### 4.4 Red Hat Code-to-Docs + +Red Hat Code-to-Docs is one of the best open implementation references. It is a GitHub Action that can be triggered by PR comments such as `[review-docs]`, `[update-docs]`, and `[review-feature]`; it analyzes PR diffs; compares code changes against docs or feature specs; and can generate docs PRs after human approval. + +The most important implementation detail is its two-stage semantic indexing: folder indexes committed under `.doc-index`, file summaries, narrowing relevant folders first, and then doing deeper analysis. Red Hat reports a reduction from roughly 20 minutes to roughly 4 minutes on large documentation sets. + +TruthMark should copy this pattern almost directly: a committed, cheap, explainable repo/doc index is more aligned with TruthMark than a hidden vector database. The difference is that TruthMark’s index should remain subordinate to canonical truth routes and evidence, not become the product’s authority. + +### 4.5 Swimm + +Swimm is mature in code-coupled documentation. Its Auto-sync algorithm analyzes PR diffs to find docs referencing changed code, auto-updates many references, and asks developers when changes are significant. Swimm stores documentation as `.swm` files in Git and integrates with CI systems such as GitHub and Azure DevOps. + +Swimm’s lesson is that the most trustworthy docs-sync systems bind documentation to code references, verify those references continuously, and preserve human review. TruthMark should copy code-coupled evidence and stale-reference detection. It should avoid becoming a proprietary authoring format unless the value of the format is overwhelming. + +### 4.6 Driver AI + +Driver is a useful newer pattern because it writes generated docs back into the repository under a dedicated `driver_docs` folder and opens PRs/MRs for review on every commit. It also closes pending docs PRs and submits a fresh combined one when changes arrive rapidly. + +The “replace stale pending docs PR with a fresh combined PR” mechanic is highly relevant. TruthMark should adapt this for an eventual GitHub Action: when multiple changes affect the same truth route, consolidate into a single truth-sync PR instead of creating PR spam. + +### 4.7 Mintlify and GitBook + +Mintlify’s agent workflow monitors code changes and existing docs style, then proposes docs PRs or pushes to a branch. Its official automation guide shows GitHub Actions calling the Mintlify agent API to update a docs repo after code changes. + +GitBook is more docs-first, but its Git Sync and Change Requests are relevant: content can be edited visually or in code, synced with GitHub/GitLab, and reviewed through branch-like change requests. GitBook Agent can also open or participate in change requests. + +TruthMark should treat both as output ecosystems. It should not try to beat them as docs authoring/publishing platforms. A stronger move is to let TruthMark govern source-grounded internal truth and publish/export into Mintlify, GitBook, Docusaurus, MkDocs, or API-doc platforms. + +### 4.8 Google Code Wiki and Cognition DeepWiki + +Google Code Wiki and Cognition DeepWiki represent the “generated repo wiki” archetype. Google Code Wiki scans the full codebase, regenerates structured docs after changes, answers chat questions using the current wiki, hyperlinks sections and answers to files/definitions, and generates architecture/class/sequence diagrams. + +DeepWiki/Devin Wiki auto-indexes repositories, generates architecture diagrams and documentation with source links, supports public repos for free, and uses `.devin/wiki.json` to steer wiki generation. Cognition said it indexed 50,000+ top public GitHub repos at launch. + +These systems are better than TruthMark at instant “what is this repo?” onboarding. They are weaker as canonical truth if their output is hosted, regenerated, or not branch-reviewed. TruthMark should not compete by building another broad hosted wiki. It should generate **derived maps** only when they are clearly marked non-canonical and tied to source spans. + +### 4.9 Greptile, Qodo, CodeRabbit, and Sourcegraph Cody + +Greptile and Qodo are not documentation products, but they are strategically important because they operationalize semantic repo understanding inside PR review. Greptile’s docs describe a codebase graph containing directories, files, functions, classes, variables, calls, imports, dependencies, and usages, then using that graph for context-aware PR review and impact analysis. + +Qodo combines code review, a context engine, and a governance rule system. Its docs describe multi-agent review in PRs, rule enforcement, full repository context, PR history, organizational standards, and a context engine that indexes code structurally, semantically, and through embeddings. + +CodeRabbit has expanded from PR review into planning, Slack agent workflows, IDE/CLI feedback, multi-repo analysis, MCP connections, and docstring generation. + +Sourcegraph Cody is the mature enterprise code-context assistant: it uses Sourcegraph’s search API to pull local and remote codebase context, including APIs, symbols, and usage patterns, into IDE and web workflows. + +TruthMark should not become a code-review bot or IDE assistant. But it must learn from these systems: semantic structure, impact analysis, rule enforcement, and PR-native feedback are table stakes for trustworthy agentic development. + +### 4.10 Agent instruction systems: AGENTS, Codex, Claude, Copilot, Gemini, opencode + +This ecosystem validates TruthMark’s multi-surface strategy. `AGENTS.md` is a shared “README for agents” format with broad client support and a claimed 60,000+ open-source projects. Codex reads scoped `AGENTS.md` files and supports Skills; Claude Code uses version-controlled `CLAUDE.md` plus memory; GitHub Copilot supports repo-wide and path-specific instructions plus `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`; Gemini CLI supports `GEMINI.md` and configurable context filenames; opencode supports `AGENTS.md`, custom instruction files, and skills. + +The critical product insight is that no single agent host will dominate every engineering team. TruthMark should become a **repository-native instruction/workflow compiler**: one canonical truth-routing contract, emitted into the instruction/rules/skills/workflows each agent host can consume. + +### 4.11 ReadMe, Fern, Stainless, Speakeasy, and API-doc MCP systems + +ReadMe shows where API docs are going: it can generate MCP servers from API docs, configure clients such as VS Code, Claude, Claude Code, ChatGPT, Gemini, and Codex, and control read/execute behavior at the route level. + +Stainless, Fern, and Speakeasy are API-doc/SDK ecosystems rather than TruthMark competitors. Stainless emphasizes full docs sites, prose, API/SDK refs, docs-as-code, Astro repos, PRs/CI, and MCP. Speakeasy emphasizes OpenAPI-derived API references, SDK samples, and MCP operation tooling. Fern has Git workflow and AI-chat/docs capabilities. + +TruthMark should copy route-level permissions and spec-to-doc validation ideas, but avoid becoming an API-platform vendor. The right integration is: TruthMark can validate and route internal truth; API platforms can publish external API docs. + +## 5. Direct competitors + +Direct competitors are products that detect documentation drift, update docs from code changes, or create docs PRs. + +| Product | Why direct | Where it is stronger than TruthMark | Where TruthMark can differ | +|---|---|---|---| +| DeepDocs | Commit-triggered docs scans and PRs | Immediate “install app, get PR” docs-sync workflow | Branch-scoped truth governance, route ownership, multi-agent workflow surfaces | +| DocSync | Automatic affected-doc updates, Markdown export, MCP | SaaS docs generation + MCP context | Local-first, repo-canonical, reviewable truth layer | +| Dosu Generate Docs | Uses diffs, PR conversations, issues, tickets | Rich input signals and issue/PR integration | Stronger route/ownership model and durable truth docs | +| Red Hat Code-to-Docs | OSS Action with PR comment commands and docs PRs | Clear implementation pattern and semantic docs index | TruthMark can productize as canonical workflow layer | +| Swimm | Code-coupled docs and Auto-sync | Mature code-reference freshness and CI integration | Broader truth workflows beyond code snippets | +| Driver | Commits docs to repo and opens PRs/MRs | Simple repo-output workflow and high-frequency PR consolidation | Explicit route ownership and agent guardrails | +| Mintlify Agent | Docs PRs/branches from code changes | Strong publishing/distribution platform | TruthMark should be upstream canonical layer | + +## 6. Adjacent but important + +The most important adjacent systems are **generated wiki/code-understanding products** and **agent instruction systems**. + +Generated wikis such as Google Code Wiki and DeepWiki will shape buyer expectations for diagrams, source links, repo Q&A, and onboarding speed. They are not canonical truth governance products, but they can make TruthMark look underpowered if TruthMark lacks RepoIndex, ImpactSet, and source-grounded ContextPack. + +Agent instruction systems are more important than most documentation platforms. AGENTS.md, CLAUDE.md, GEMINI.md, Copilot instructions, Codex Skills, opencode skills, Packmind, and `.agents` CLI all point to the same conclusion: teams need **versioned, reusable, tool-specific agent behavior surfaces**. TruthMark already has the right direction; it needs deeper compilation, validation, and drift detection. + +## 7. Not actually competitors + +Docusaurus, MkDocs, TypeDoc, JSDoc, OpenAPI Generator, Swagger UI, Redocly, and Stoplight are mostly substrates. They generate or publish docs, but they do not solve branch-scoped truth governance, agent write boundaries, route ownership, or stale-doc impact from code changes. Docusaurus and MkDocs are static docs frameworks; TypeDoc and JSDoc generate API docs from TS/JS; OpenAPI Generator/Swagger/Redocly/Stoplight operate around API specs and docs rendering/linting. + +They matter as integration targets and implementation references. TruthMark should emit or validate against them, not compete with them. + +## 8. Product-pattern analysis: what the market is converging on + +| Pattern | Evidence | Implication for TruthMark | +|---|---|---| +| Docs updates are moving into PR workflows | DeepDocs, Driver, Mintlify, Red Hat, Swimm all use PR/branch/comment/CI patterns. | TruthMark needs first-party GitHub Action examples and PR-native diagnostics. | +| “Agent context” is becoming a product category | Unblocked, Pieces, Context7, ReadMe MCP, Qodo Context Engine, Codex Skills all expose context to agents. | TruthMark should produce bounded ContextPacks for agents. | +| Source links and citations are becoming trust primitives | Code Wiki and DeepWiki emphasize source links; Unblocked says answers draw from sources; AgenticCodebase has grounding/evidence tools. | TruthMark’s future moat should be claim-to-source validation. | +| Semantic structure is replacing plain RAG | Greptile graph, Qodo structural/semantic/embedding index, AgenticCodebase graph, Red Hat semantic folder/file index. | Build RepoIndex before adding broad generation. | +| Multi-agent support is no longer optional | AGENTS.md, Copilot, Codex, Claude, Gemini, opencode all have distinct but overlapping instruction systems. | TruthMark should compile one truth contract into many agent surfaces. | +| SaaS indexes are common, but not always ideal | Sourcegraph, Greptile, DocSync, Unblocked, CodeSee, Qodo rely on indexed service models; Pieces and Doclific are more local-first. | Local-first/no-daemon remains a real adoption advantage if functionality is sufficient. | +| Human review remains the trust boundary | Red Hat, Dosu, DeepDocs, Driver, Mintlify, GitBook all preserve review/PR/change-request flows. | TruthMark should default to suggestions/PRs, not direct commits. | + +## 9. Implementation-pattern analysis: technical ideas TruthMark should study + +TruthMark should implement **RepoIndex v0** as a deterministic, committed or cacheable structure: files, packages, routes, exports, imports, symbols, tests, docs, and ownership routes. Red Hat’s `.doc-index` pattern is the closest docs-specific reference, while AgenticCodebase demonstrates a more aggressive graph artifact with symbols, imports, call chains, type relationships, impact analysis, evidence, and MCP access. + +TruthMark should implement **ImpactSet** as the core of Truth Sync: from a Git diff, compute changed files, changed symbols, affected routes, related docs, related tests, and affected owners. Greptile’s graph model and Qodo’s structural/semantic/embedding context engine show why this matters for PR-quality analysis. + +TruthMark should implement **ContextPack** as a bounded artifact for agents: task instructions, relevant truth docs, source spans, changed files, route ownership, test commands, and “do not write outside these paths.” Context7’s dual “CLI + Skills or MCP” model is a useful pattern: make context accessible without forcing an MCP dependency. + +TruthMark should implement **claim-to-source validation**. A claim in a truth doc should be traceable to file paths, line spans, symbols, routes, tests, schemas, or PRs. TypeDoc JSON output, JSDoc comments, OpenAPI specs, Redocly linting, and AgenticCodebase grounding/evidence all suggest implementation building blocks. + +TruthMark should implement **freshness diagnostics** rather than full auto-rewrite first: changed route has no truth doc; truth doc references deleted symbol; evidence span no longer exists; owner missing; stale frontmatter timestamp; changed public API lacks docs sync; generated claim lacks evidence. This gives immediate value and preserves human review. + +## 10. Differentiation analysis: where TruthMark can own a unique position + +TruthMark can own a narrower category than most competitors: **branch-scoped, Git-reviewable truth infrastructure for multi-agent software development**. + +The defensible elements are: + +| Differentiator | Why it matters | Current evidence / gap | +|---|---|---| +| Branch-scoped truth | Agents work on branches; truth must match branch state, not main/SaaS cache | Already core to repo positioning. | +| Route ownership | Broad repo summaries do not tell agents who owns what or where truth should be edited | Already present through `docs/truthmark/areas.md`; needs enforcement. | +| Git-reviewable canonical docs | Human review is still the market trust boundary | Competitors also use PRs; TruthMark must make this central. | +| Multi-agent workflow surfaces | Teams will use Codex, Claude, Copilot, Gemini, and opencode together | Agent ecosystem validates this strongly. | +| Source-grounded claims | Generated docs without evidence become another stale artifact | Code Wiki, DeepWiki, Unblocked, AgenticCodebase all point toward citations/evidence. | +| Local-first/no service required | Reduces adoption friction and privacy objections | Already a TruthMark property; competitors often use SaaS indexes. | + +## 11. Threat analysis + +The biggest direct threat is that **DocSync, DeepDocs, Driver, Dosu, or Mintlify** could add route ownership, agent instruction generation, and stronger source citations. If they do, “AI docs sync” becomes good enough for many teams, and TruthMark must compete on governance rather than generation. + +The biggest adjacent threat is that **Google Code Wiki or DeepWiki** becomes the default way engineers and agents understand repositories. If private/local support matures, teams may accept generated hosted wikis as the working truth even if they are not branch-reviewable. + +The biggest platform threat is **GitHub Copilot/Codex/Claude** standardizing enough repo instruction behavior that teams no longer seek a separate workflow layer. TruthMark’s defense is to become complementary: it writes and validates those surfaces instead of competing with the hosts. + +The biggest technical threat is lack of semantic understanding. Without RepoIndex, ImpactSet, and evidence validation, TruthMark risks being perceived as a set of templates and conventions while competitors ship graphs, PR impact analysis, and grounded context. + +The biggest trust threat is unsupported claims. If TruthMark truth docs can drift or hallucinate without file/test/route evidence, the product name becomes a liability. + +## 12. Opportunity analysis: gaps competitors leave open + +| Gap | Why competitors leave it open | TruthMark opportunity | +|---|---|---| +| Canonical truth committed to branch | SaaS indexes and hosted wikis are easier to build | Make Git the trust boundary. | +| Explicit truth routing and ownership | Most tools summarize broadly | Enforce route ownership and scoped edit surfaces. | +| Multi-agent workflow compilation | Agent hosts optimize for their own formats | Emit AGENTS/CLAUDE/GEMINI/Copilot/opencode from one contract. | +| Claim-to-source validation | Harder than generation | Build evidence spans and CI checks as moat. | +| Local-first adoption | SaaS vendors monetize indexes | Keep no-daemon/no-database default; optional integrations later. | +| Sync-after-code plus implementation-first workflows | Docs tools focus on docs updates | Frame TruthMark as a workflow guardrail for agents implementing code. | +| Reviewable ContextPack artifacts | Context products often hide retrieval | Generate auditable context packs for agent runs. | + +## 13. Recommended roadmap changes + +### Immediate + +1. Add **RepoIndex v0** for file tree, docs tree, package metadata, imports/exports, route ownership, test files, and public symbols for the most common first language stack. +2. Add **ImpactSet v0**: `truthmark impact --base main` should map changed files to truth routes, docs, owners, and tests. +3. Add **freshness diagnostics** to `truthmark check`: changed code with no routed truth doc, stale evidence, missing owner, deleted referenced file, changed API without docs sync. +4. Add **ContextPack v0**: generate bounded agent context for Truth Sync, Truth Document, and Truth Realize workflows. +5. Expand instruction-surface generation beyond AGENTS/CLAUDE/GEMINI into Copilot instructions, opencode skills, and Codex-compatible Skills. +6. Add a **GitHub Action example** with non-blocking and blocking modes: comment on PR, attach ImpactSet report, optionally fail on missing truth route or stale evidence. +7. Add a **docs-map / route-map schema** inspired by Dosu’s docs-map pattern and Red Hat’s `.doc-index`, but aligned to TruthMark’s `areas.md`. + +### Medium-term + +1. Add **source-grounded claim schema**: each truth claim can cite file path, line span, symbol, route, test, schema, or PR. +2. Add **truth-sync PR mode**: generate a branch with suggested truth-doc changes, never direct-commit by default. +3. Add **parser adapters**: TypeScript/JavaScript first, then Python, Go, Java/C#, using tree-sitter or language-native analyzers where practical. +4. Add **route detectors** for common frameworks: Next.js, Express/Fastify, Rails, Django/FastAPI, Spring, ASP.NET. +5. Add **derived non-canonical maps**: architecture summaries, route maps, Mermaid diagrams, and wiki-like pages clearly marked as generated/derived. +6. Add **skill packs**: Truth Sync skill, Truth Review skill, Truth Realize skill, Truth Repair skill. +7. Add **optional MCP read-only server**, but preserve no-MCP default. + +### Long-term + +1. Build **claim validation CI** with evidence-span refresh and stale-claim detection. +2. Support **cross-repo truth** for monorepos and multi-service systems. +3. Add **ticket/spec/docs ingestion** for Jira, Linear, Confluence, Google Docs, and API specs, but keep source provenance explicit. +4. Add **enterprise policy packs**: required owners, required evidence classes, protected truth areas, branch protection integration. +5. Build **truth review analytics**: stale routes, missing owners, agent edits outside allowed scope, most frequently invalidated claims. +6. Explore **local semantic graph artifact** similar to AgenticCodebase but smaller, auditable, and TruthMark-specific. + +## 14. Copy / adapt / avoid table + +| Source | Copy | Adapt | Avoid | +|---|---|---|---| +| DeepDocs | Commit-triggered docs PRs, code/docs mapping | Style-preserving edits | Unclear app status and opaque SaaS-only workflow | +| DocSync | Exportable Markdown, MCP access | Optional read-only TruthMark MCP | Making generated docs the only truth | +| Dosu | Use diffs, PRs, issues, tickets as signals | Freshness score decomposed into diagnostics | Black-box scoring without actionable evidence | +| Red Hat Code-to-Docs | PR comment commands, `.doc-index`, semantic folder/file narrowing | Multi-LLM support after core local workflow | Overbuilding LLM orchestration before deterministic checks | +| Swimm | Code-coupled docs, CI freshness checks | Smart snippets/evidence spans | Proprietary doc format as the main moat | +| Driver | Repo folder output, PR/MR review, consolidated updates | Truth-sync branch management | Commit-per-change PR spam | +| Google Code Wiki / DeepWiki | Diagrams, source links, generated maps | Derived non-canonical repo maps | Hosted generated wiki as canonical truth | +| Greptile / Qodo | Graph context, impact analysis, rule enforcement | Route-aware ImpactSet | Becoming a code-review product | +| AGENTS/Codex/Claude/Copilot/Gemini/opencode | Multi-surface agent instructions | TruthMark instruction compiler | Depending on one agent host | +| ReadMe/Fern/Stainless/Speakeasy | Route-level MCP/API controls | API-spec evidence integration | Becoming an API-doc platform | +| Context7 | CLI+Skills or MCP dual path | TruthMark context through both skill and optional MCP | MCP dependency for core adoption | +| AgenticCodebase | Local semantic graph and grounding | Smaller TruthMark-specific graph/evidence model | Huge binary graph as required dependency | + +## 15. Positioning options + +### One-sentence positioning + +**TruthMark turns repository truth into branch-scoped, Git-reviewable agent infrastructure: routed ownership, bounded workflow surfaces, and source-grounded maintenance after code changes.** + +### One-paragraph positioning + +**TruthMark is not an AI documentation generator. It installs a repository-native truth layer for AI-assisted software development: canonical truth docs committed to the branch, explicit route ownership, agent-specific workflow surfaces, and checks that keep truth synchronized after code changes. Instead of trapping context in chat, SaaS indexes, generated wikis, or personal memory, TruthMark makes team truth durable, reviewable, and usable by Codex, Claude Code, Copilot, Gemini, opencode, and future agents.** + +### Landing-page hero version + +**Keep agents aligned with the truth in your repo.** +TruthMark installs branch-scoped, Git-reviewable truth workflows for AI-assisted development: route ownership, bounded agent instructions, source-grounded docs, and sync checks after code changes. + +### Technical-founder version + +**TruthMark is a local-first truth governance layer for agentic software development. It maps code changes to owned truth routes, builds bounded context packs for coding agents, emits host-specific instruction surfaces, and validates truth claims against source files, symbols, tests, and PRs.** + +### Enterprise-buyer version + +**TruthMark gives engineering teams a reviewable control plane for AI coding workflows. It keeps system truth in Git, assigns ownership, limits where agents should write, preserves human review, and produces auditable evidence that documentation and agent context match the code being shipped.** + +## 16. Hypotheses tested + +| Hypothesis | Finding | +|---|---| +| Branch-scoped, Git-reviewable truth is more defensible than hosted generated docs | Supported. Many competitors use PRs, but generated wikis/SaaS indexes rarely make branch-scoped canonical truth the center. | +| Agent workflow guardrail is more differentiated than “AI writes docs” | Supported. Agent instruction fragmentation creates a real need for repo-governed workflow surfaces. | +| Truth routing and ownership may matter more than broad summarization | Supported but must be proven in product UX. Broad summarizers are common; route ownership is rarer. | +| Source-grounded claim validation may be the strongest future moat | Strongly supported. Market is moving toward source links, code graphs, and evidence. | +| Multi-agent workflow surfaces matter | Strongly supported by AGENTS.md, Codex, Claude, Copilot, Gemini, and opencode ecosystems. | +| Local-first/no daemon/database/MCP dependency is an adoption advantage | Supported for small teams and privacy-sensitive teams, but TruthMark still needs optional MCP/export integrations. | +| RepoIndex + ImpactSet + ContextPack is the right implementation direction | Supported. Multiple competitors and OSS tools converge on semantic indexes, impact sets, and context retrieval. | + +## 17. Source appendix + +TruthMark baseline and prior note: TruthMark repo positioning, installed files, local-first model, supported agent surfaces, and existing RepoIndex/ImpactSet/ContextPack recommendations. + +Continuous docs sync: DeepDocs, DocSync, Dosu Generate Docs, Dosu freshness scoring, Red Hat Code-to-Docs, Swimm, Driver, Mintlify, GitBook. + +Codebase wiki, code understanding, and review/context products: Google Code Wiki, Cognition DeepWiki/Devin Wiki, Sourcegraph Cody, Greptile, CodeSee, Pieces, Unblocked, Qodo, CodeRabbit, Bloop, Davia, OpenDeepWiki. + +Agent instruction and workflow systems: AGENTS.md, Codex AGENTS.md and Skills, Claude Code memory, GitHub Copilot instructions, Gemini CLI, opencode rules/skills, `.agents` CLI, Packmind. + +Docs-as-code, API docs, and implementation references: ReadMe MCP, Fern/Stainless/Speakeasy, Docusaurus, MkDocs, TypeDoc, JSDoc, OpenAPI Generator, Swagger UI, Redocly, Stoplight Elements, Context7, AgenticCodebase. diff --git a/src/agents/instructions.ts b/src/agents/instructions.ts index 0806c18..eef3d02 100644 --- a/src/agents/instructions.ts +++ b/src/agents/instructions.ts @@ -35,7 +35,7 @@ Explicit invocation runs immediately: ${TRUTH_SYNC_EXPLICIT_INVOCATIONS} Later functional-code changes reopen the finish-time requirement, and an earlier explicit run only satisfies the finish gate if no later functional-code changes occur. Memory anchor: code changed -> relevant tests -> Truth Sync -> report. Delegate to a subagent only when the host supports subagent dispatch; the acting agent and environment own that choice. -Inspect the current checkout directly. Do not invoke packet helpers or rely on cache files. +Inspect the current checkout directly. ImpactSet and ContextPack are optional derived context; they do not override checkout evidence or write boundaries. Run relevant tests before finishing when functional code changes occurred. Support new or changed behavior-bearing truth claims with checkout evidence and report the evidence reviewed. Truthmark is agent-native: installed skills and this managed block are the workflow runtime. Inspect the checkout directly; truthmark CLI commands are optional validation tools after installation. diff --git a/src/agents/prompts.ts b/src/agents/prompts.ts index 6bf62eb..e393f72 100644 --- a/src/agents/prompts.ts +++ b/src/agents/prompts.ts @@ -2,6 +2,7 @@ import type { TruthmarkConfig } from "../config/schema.js"; import { renderTruthRealizeCompletedReport } from "../realize/report.js"; import { EVIDENCE_AUTHORITY_INSTRUCTIONS, + REPOSITORY_INTELLIGENCE_INSTRUCTIONS, defaultAgentConfig, renderHierarchySummary, renderTruthDocOwnershipGateSection, @@ -23,6 +24,7 @@ Invocations: OpenCode /skill truthmark-realize; Codex /truthmark-realize or $tru Doc first: - read the updated truth docs plus .truthmark/config.yml, route files, relevant code, and tests - ${EVIDENCE_AUTHORITY_INSTRUCTIONS} +- ${REPOSITORY_INTELLIGENCE_INSTRUCTIONS} ${renderTruthDocOwnershipGateSection( "source truth docs before writing code", "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", @@ -45,6 +47,6 @@ export const renderTruthRealizeInstructions = (): string => { return `### Manual Truth Realize Only run when the user explicitly asks to realize truth docs into code. This is a manual installed instruction or skill, not a dedicated CLI command. Invocations: OpenCode /skill truthmark-realize; Codex /truthmark-realize or $truthmark-realize; Claude Code /truthmark-realize; GitHub Copilot /truthmark-realize; Gemini CLI /truthmark:realize. -Doc first: read truth docs, routing, and relevant code; block before writing code if source truth is broad, mixed-owner, index-like, unrouteable, stale, or conflicts with implementation evidence; write functional code only; do not edit truth docs or truth routing. +Doc first: read truth docs, routing, and relevant code; repository-intelligence artifacts are optional derived context and never grant write scope; block before writing code if source truth is broad, mixed-owner, index-like, unrouteable, stale, or conflicts with implementation evidence; write functional code only; do not edit truth docs or truth routing. Report truth docs used, code updated, and verification.`; }; diff --git a/src/agents/shared.ts b/src/agents/shared.ts index cce2e9b..2573997 100644 --- a/src/agents/shared.ts +++ b/src/agents/shared.ts @@ -25,6 +25,12 @@ export const EVIDENCE_AUTHORITY_INSTRUCTIONS = [ "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries.", ].join("\n"); +export const REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [ + "Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available.", + "They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.", + "If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated.", +].join("\n"); + export const FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [ "When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/-doc.md` template.", "Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior.", diff --git a/src/agents/truth-document.ts b/src/agents/truth-document.ts index 9c73492..915d164 100644 --- a/src/agents/truth-document.ts +++ b/src/agents/truth-document.ts @@ -4,6 +4,7 @@ import { DECISION_TRUTH_INSTRUCTIONS, EVIDENCE_AUTHORITY_INSTRUCTIONS, FEATURE_DOC_TEMPLATE_INSTRUCTIONS, + REPOSITORY_INTELLIGENCE_INSTRUCTIONS, TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS, defaultAgentConfig, renderClaimEvidenceCheckedSection, @@ -102,6 +103,7 @@ ${renderRouteFirstEvidenceGateSection( "the documented behavior", "if no truth doc changed, report why current truth was already sufficient or why documentation was blocked", )} +${REPOSITORY_INTELLIGENCE_INSTRUCTIONS} ${FEATURE_DOC_TEMPLATE_INSTRUCTIONS} ${renderTruthDocRestructureGateSection( "Truth Document may restructure only truth docs for the implemented behavior being documented.", diff --git a/src/agents/truth-sync.ts b/src/agents/truth-sync.ts index cb3be2e..5e95834 100644 --- a/src/agents/truth-sync.ts +++ b/src/agents/truth-sync.ts @@ -4,6 +4,7 @@ import { DECISION_TRUTH_INSTRUCTIONS, EVIDENCE_AUTHORITY_INSTRUCTIONS, FEATURE_DOC_TEMPLATE_INSTRUCTIONS, + REPOSITORY_INTELLIGENCE_INSTRUCTIONS, TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS, defaultAgentConfig, renderRouteFirstEvidenceGateSection, @@ -99,6 +100,7 @@ ${renderRouteFirstEvidenceGateSection( "changed functional files", "if no impacted doc changed, report why truth was already current or why sync was skipped", )} +${REPOSITORY_INTELLIGENCE_INSTRUCTIONS} Optional validation tooling: - you may run truthmark check when local tooling is available - do not require the truthmark binary; direct checkout inspection is the canonical path diff --git a/src/checks/check.ts b/src/checks/check.ts index 4617399..999c7f7 100644 --- a/src/checks/check.ts +++ b/src/checks/check.ts @@ -8,6 +8,11 @@ import { checkLinks } from "./links.js"; import { checkAreas } from "./areas.js"; import { checkDecisionSections } from "./decisions.js"; import { checkGeneratedSurfaces } from "./generated-surfaces.js"; +import { checkFreshness } from "../freshness/check.js"; + +export type CheckOptions = { + base?: string; +}; const summarizeDiagnostics = (diagnostics: CommandResult["diagnostics"]): string => { const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length; @@ -20,7 +25,7 @@ const summarizeDiagnostics = (diagnostics: CommandResult["diagnostics"]): string return `Truthmark check completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`; }; -export const runCheck = async (cwd: string): Promise => { +export const runCheck = async (cwd: string, options: CheckOptions = {}): Promise => { const repository = await getGitRepository(cwd); const rootDir = repository.worktreePath; const branchScope = await getBranchScopeData(rootDir); @@ -54,6 +59,9 @@ export const runCheck = async (cwd: string): Promise => { areas.truthDocumentEntries, ); const generatedSurfaces = await checkGeneratedSurfaces(rootDir, loadResult.config); + const freshness = options.base + ? await checkFreshness(rootDir, loadResult.config, areas.truthDocumentPaths, options.base) + : null; const diagnostics = [ ...loadResult.diagnostics, ...authority.diagnostics, @@ -62,6 +70,7 @@ export const runCheck = async (cwd: string): Promise => { ...areas.diagnostics, ...decisionSections, ...generatedSurfaces, + ...(freshness?.diagnostics ?? []), ]; const truthVisibility = { routePrecision: areas.routePrecision, @@ -74,7 +83,8 @@ export const runCheck = async (cwd: string): Promise => { (diagnostic) => diagnostic.category === "doc-structure" || diagnostic.category === "generated-surface", ).length, - topologyPressureCount: areas.topologyPressureCount, + topologyPressureCount: areas.topologyPressureCount, + freshnessDiagnosticCount: freshness?.diagnostics.length ?? 0, }; return { @@ -84,6 +94,7 @@ export const runCheck = async (cwd: string): Promise => { data: { branchScope, truthVisibility, + ...(freshness ? { impactSet: freshness.impactSet } : {}), }, }; }; diff --git a/src/cli/handlers.ts b/src/cli/handlers.ts index 7f14fb0..b0abcb2 100644 --- a/src/cli/handlers.ts +++ b/src/cli/handlers.ts @@ -2,6 +2,11 @@ import { runConfig as runRepositoryConfig, type ConfigCommandOptions } from "../ import { runInit as runRepositoryInit } from "../init/init.js"; import { runCheck as runRepositoryCheck } from "../checks/check.js"; import type { CommandResult } from "../output/diagnostic.js"; +import { buildImpactSet } from "../impact/build.js"; +import { buildContextPack } from "../context-pack/build.js"; +import { renderContextPackMarkdown } from "../context-pack/render.js"; +import type { ContextPackWorkflow } from "../context-pack/types.js"; +import { buildRepoIndex } from "../repo-index/build.js"; export const runConfig = async (options: ConfigCommandOptions): Promise => { return runRepositoryConfig(process.cwd(), options); @@ -11,6 +16,109 @@ export const runInit = async (): Promise => { return runRepositoryInit(process.cwd()); }; -export const runCheck = async (): Promise => { - return runRepositoryCheck(process.cwd()); +export const runCheck = async (options: { base?: string } = {}): Promise => { + return runRepositoryCheck(process.cwd(), options); +}; + +export const runIndex = async (): Promise => { + const repoIndex = await buildRepoIndex(process.cwd()); + const errorCount = repoIndex.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length; + const reviewCount = repoIndex.diagnostics.filter((diagnostic) => diagnostic.severity === "review").length; + + return { + command: "index", + summary: `Truthmark index completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`, + diagnostics: repoIndex.diagnostics, + data: { + repoIndex, + routeMap: repoIndex.routeMap, + }, + }; +}; + +export const runImpact = async (options: { base?: string }): Promise => { + if (!options.base) { + return { + command: "impact", + summary: "Truthmark impact requires --base.", + diagnostics: [ + { + category: "impact", + severity: "error", + message: "truthmark impact requires --base .", + }, + ], + }; + } + + const impactSet = await buildImpactSet(process.cwd(), { base: options.base }); + const errorCount = impactSet.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length; + const reviewCount = impactSet.diagnostics.filter((diagnostic) => diagnostic.severity === "review").length; + + return { + command: "impact", + summary: `Truthmark impact completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`, + diagnostics: impactSet.diagnostics, + data: { + impactSet, + }, + }; +}; + +const isContextPackWorkflow = (value: unknown): value is ContextPackWorkflow => { + return value === "truth-sync" || value === "truth-document" || value === "truth-realize"; +}; + +const isContextPackFormat = (value: unknown): value is "json" | "markdown" | undefined => { + return value === undefined || value === "json" || value === "markdown"; +}; + +export const runContext = async (options: { + workflow?: string; + base?: string; + format?: string; +}): Promise => { + if (!isContextPackWorkflow(options.workflow)) { + return { + command: "context", + summary: "Truthmark context requires a supported --workflow value.", + diagnostics: [ + { + category: "context-pack", + severity: "error", + message: "truthmark context requires --workflow truth-sync, truth-document, or truth-realize.", + }, + ], + }; + } + + if (!isContextPackFormat(options.format)) { + return { + command: "context", + summary: "Truthmark context requires a supported --format value.", + diagnostics: [ + { + category: "context-pack", + severity: "error", + message: "truthmark context requires --format json or markdown.", + }, + ], + }; + } + + const contextPack = await buildContextPack(process.cwd(), { + workflow: options.workflow, + base: options.base, + }); + const diagnostics = contextPack.warnings; + + return { + command: "context", + summary: `Truthmark context generated ${contextPack.workflow} ContextPack with ${diagnostics.length} warnings.`, + diagnostics, + data: { + contextPack, + ...(options.format === "markdown" ? { markdown: renderContextPackMarkdown(contextPack) } : {}), + }, + }; }; diff --git a/src/cli/program.ts b/src/cli/program.ts index b16e26b..0a36081 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import type { CommandResult } from "../output/diagnostic.js"; import { renderHuman, renderJson } from "../output/render.js"; -import { runCheck, runConfig, runInit } from "./handlers.js"; +import { runCheck, runConfig, runContext, runImpact, runIndex, runInit } from "./handlers.js"; type OutputOptions = { json?: boolean; @@ -13,10 +13,31 @@ type ConfigOptions = OutputOptions & { force?: boolean; }; +type CheckCliOptions = OutputOptions & { + base?: string; +}; + +type ImpactOptions = OutputOptions & { + base?: string; +}; + +type ContextOptions = OutputOptions & { + workflow?: string; + base?: string; + format?: string; +}; + const writeResult = (result: CommandResult, options: OutputOptions): void => { const output = options.json ? renderJson(result) : renderHuman(result); process.stdout.write(`${output}\n`); }; +const writeContextResult = (result: CommandResult, options: ContextOptions): void => { + if (!options.json && options.format === "markdown" && typeof result.data?.markdown === "string") { + process.stdout.write(result.data.markdown); + return; + } + writeResult(result, options); +}; const addJsonOption = (command: Command): Command => { return command.option("--json", "Render command output as JSON"); @@ -49,9 +70,45 @@ export const buildProgram = (): Command => { }); addJsonOption( - program.command("check").description("Run local Truthmark diagnostics."), + program + .command("check") + .description("Run local Truthmark diagnostics.") + .option("--base ", "Base Git ref for freshness diagnostics"), + ).action(async (options: CheckCliOptions) => { + writeResult(await runCheck({ base: options.base }), options); + }); + + addJsonOption( + program.command("index").description("Build the deterministic Truthmark repository index."), ).action(async (options: OutputOptions) => { - writeResult(await runCheck(), options); + writeResult(await runIndex(), options); + }); + + addJsonOption( + program + .command("impact") + .description("Map changed files to truth routes, docs, owners, and tests.") + .requiredOption("--base ", "Base Git ref to compare against"), + ).action(async (options: ImpactOptions) => { + writeResult(await runImpact({ base: options.base }), options); + }); + + addJsonOption( + program + .command("context") + .description("Generate a bounded workflow context pack.") + .requiredOption("--workflow ", "Workflow name: truth-sync, truth-document, or truth-realize") + .option("--base ", "Base Git ref for impact-backed packs") + .option("--format ", "Output format: json or markdown", "json"), + ).action(async (options: ContextOptions) => { + writeContextResult( + await runContext({ + workflow: options.workflow, + base: options.base, + format: options.format, + }), + options, + ); }); return program; diff --git a/src/context-pack/build.ts b/src/context-pack/build.ts new file mode 100644 index 0000000..077a840 --- /dev/null +++ b/src/context-pack/build.ts @@ -0,0 +1,207 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import fg from "fast-glob"; + +import { buildImpactSet } from "../impact/build.js"; +import type { ImpactSet } from "../impact/types.js"; +import type { Diagnostic } from "../output/diagnostic.js"; +import { buildRepoIndex } from "../repo-index/build.js"; +import type { RepoDocEntry, RouteMap } from "../repo-index/types.js"; +import type { + ContextDocument, + ContextPack, + ContextPackOptions, + ContextSourceFile, +} from "./types.js"; + +const uniqueSorted = (values: string[]): string[] => [...new Set(values)].sort(); + +type ContextRoute = { + codeSurface: string[]; +}; + +const repoRootPrefixes = [".codex/", ".github/", ".truthmark/", "docs/", "src/", "tests/"]; + +const isGlobReference = (referencePath: string): boolean => /[*?[\]{}()]/u.test(referencePath); + +const normalizeDocReferencePath = (docPath: string, referencePath: string): string | null => { + const strippedPath = referencePath.split("#")[0]?.trim() ?? ""; + if (strippedPath.length === 0 || strippedPath.startsWith("/")) { + return null; + } + + const isRepoRelative = repoRootPrefixes.some((prefix) => strippedPath.startsWith(prefix)); + const normalized = isRepoRelative + ? path.posix.normalize(strippedPath) + : path.posix.normalize(path.posix.join(path.posix.dirname(docPath), strippedPath)); + + return normalized === ".." || normalized.startsWith("../") ? null : normalized; +}; + +const readIfExists = async (rootDir: string, filePath: string): Promise => { + try { + return await fs.readFile(path.join(rootDir, filePath), "utf8"); + } catch { + return null; + } +}; + +const boundedContent = ( + filePath: string, + content: string, + warnings: Diagnostic[], +): ContextSourceFile => { + const lines = content.split("\n"); + + if (lines.length <= 200) { + return { path: filePath, content, truncated: false }; + } + + warnings.push({ + category: "context-pack", + severity: "review", + message: `Context source file ${filePath} was truncated to fit ContextPack v0 bounds.`, + file: filePath, + }); + + return { + path: filePath, + content: [...lines.slice(0, 80), "...", ...lines.slice(-40)].join("\n"), + truncated: true, + }; +}; + +const documentsFor = async ( + rootDir: string, + paths: string[], +): Promise => { + const documents: ContextDocument[] = []; + + for (const filePath of uniqueSorted(paths)) { + const content = await readIfExists(rootDir, filePath); + if (content !== null) { + documents.push({ path: filePath, content }); + } + } + + return documents; +}; + +const sourceFilesFor = async ( + rootDir: string, + paths: string[], + warnings: Diagnostic[], +): Promise => { + const sourceFiles: ContextSourceFile[] = []; + + for (const filePath of uniqueSorted(paths)) { + const content = await readIfExists(rootDir, filePath); + if (content !== null) { + sourceFiles.push(boundedContent(filePath, content, warnings)); + } + } + + return sourceFiles; +}; + +const sourceOfTruthPathsFor = async ( + rootDir: string, + docs: RepoDocEntry[], + truthDocPaths: string[], +): Promise => { + const selectedTruthDocs = new Set(truthDocPaths); + const sourcePaths: string[] = []; + + for (const doc of docs) { + if (!selectedTruthDocs.has(doc.path)) { + continue; + } + + for (const referencePath of doc.sourceOfTruth) { + const normalizedPath = normalizeDocReferencePath(doc.path, referencePath); + if (!normalizedPath) { + continue; + } + if (isGlobReference(normalizedPath)) { + sourcePaths.push( + ...(await fg(normalizedPath, { + cwd: rootDir, + dot: true, + onlyFiles: true, + followSymbolicLinks: false, + })), + ); + } else { + sourcePaths.push(normalizedPath); + } + } + } + + return uniqueSorted(sourcePaths); +}; + +const writePathsFor = ( + workflow: ContextPackOptions["workflow"], + truthDocs: string[], + routes: ContextRoute[], +): string[] => { + if (workflow === "truth-sync") { + return uniqueSorted(["docs/truthmark/areas.md", ...truthDocs]); + } + + if (workflow === "truth-document") { + return uniqueSorted(["docs/truthmark/areas.md", ...truthDocs]); + } + + return uniqueSorted(routes.flatMap((route) => route.codeSurface)); +}; + +const testCommandsFor = (affectedTests: string[]): string[] => { + return affectedTests.length === 0 + ? ["npm test"] + : [`npm test -- ${affectedTests.join(" ")}`]; +}; + +export const buildContextPack = async ( + cwd: string, + options: ContextPackOptions, +): Promise => { + const repoIndex = await buildRepoIndex(cwd); + const rootDir = repoIndex.repository.root; + const impactSet: ImpactSet | null = options.base + ? await buildImpactSet(rootDir, { base: options.base }) + : null; + const routeMap: RouteMap = impactSet ? repoIndex.routeMap : repoIndex.routeMap; + const warnings: Diagnostic[] = []; + const truthDocPaths = + impactSet?.affectedTruthDocs ?? + (options.workflow === "truth-realize" ? [] : routeMap.routes.flatMap((route) => route.truthDocs)); + const contextRoutes: ContextRoute[] = + impactSet?.affectedRoutes ?? (options.workflow === "truth-realize" ? [] : routeMap.routes); + if (options.workflow === "truth-realize" && !impactSet) { + warnings.push({ + category: "context-pack", + severity: "review", + message: "truth-realize requires --base to derive bounded allowed write paths.", + }); + } + const sourceOfTruthPaths = await sourceOfTruthPathsFor(rootDir, repoIndex.docs, truthDocPaths); + const sourceFilePaths = uniqueSorted([ + ...(impactSet?.changedFiles.filter((file) => !file.deleted).map((file) => file.path) ?? []), + ...sourceOfTruthPaths, + ]); + + return { + schemaVersion: "context-pack/v0", + workflow: options.workflow, + base: options.base ?? null, + impactSet, + routeMap, + allowedWritePaths: writePathsFor(options.workflow, truthDocPaths, contextRoutes), + truthDocs: await documentsFor(rootDir, truthDocPaths), + sourceFiles: await sourceFilesFor(rootDir, sourceFilePaths, warnings), + testCommands: testCommandsFor(impactSet?.affectedTests ?? []), + warnings, + }; +}; diff --git a/src/context-pack/render.ts b/src/context-pack/render.ts new file mode 100644 index 0000000..526ff93 --- /dev/null +++ b/src/context-pack/render.ts @@ -0,0 +1,24 @@ +import type { ContextPack } from "./types.js"; + +export const renderContextPackMarkdown = (pack: ContextPack): string => { + const lines = [ + `# Truthmark ContextPack (${pack.workflow})`, + "", + `Schema: ${pack.schemaVersion}`, + `Base: ${pack.base ?? "none"}`, + "", + "## Allowed Write Paths", + ...pack.allowedWritePaths.map((filePath) => `- ${filePath}`), + "", + "## Truth Docs", + ...pack.truthDocs.map((doc) => `- ${doc.path}`), + "", + "## Source Files", + ...pack.sourceFiles.map((file) => `- ${file.path}${file.truncated ? " (truncated)" : ""}`), + "", + "## Test Commands", + ...pack.testCommands.map((command) => `- ${command}`), + ]; + + return `${lines.join("\n")}\n`; +}; diff --git a/src/context-pack/types.ts b/src/context-pack/types.ts new file mode 100644 index 0000000..2aa60f2 --- /dev/null +++ b/src/context-pack/types.ts @@ -0,0 +1,34 @@ +import type { Diagnostic } from "../output/diagnostic.js"; +import type { ImpactSet } from "../impact/types.js"; +import type { RouteMap } from "../repo-index/types.js"; + +export type ContextPackWorkflow = "truth-sync" | "truth-document" | "truth-realize"; + +export type ContextDocument = { + path: string; + content: string; +}; + +export type ContextSourceFile = { + path: string; + content: string; + truncated: boolean; +}; + +export type ContextPack = { + schemaVersion: "context-pack/v0"; + workflow: ContextPackWorkflow; + base: string | null; + impactSet: ImpactSet | null; + routeMap: RouteMap; + allowedWritePaths: string[]; + truthDocs: ContextDocument[]; + sourceFiles: ContextSourceFile[]; + testCommands: string[]; + warnings: Diagnostic[]; +}; + +export type ContextPackOptions = { + workflow: ContextPackWorkflow; + base?: string; +}; diff --git a/src/evidence/parse.ts b/src/evidence/parse.ts new file mode 100644 index 0000000..5e0fda9 --- /dev/null +++ b/src/evidence/parse.ts @@ -0,0 +1,85 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import matter from "gray-matter"; +import { parse } from "yaml"; + +import type { EvidenceReference } from "./types.js"; + +const evidenceBlockPattern = /```ya?ml\s*\n([\s\S]*?)```/giu; + +const repoRootPrefixes = [".codex/", ".github/", ".truthmark/", "docs/", "src/", "tests/"]; + +const normalizeReferencePath = (truthDocPath: string, referencePath: string): string => { + const strippedPath = referencePath.split("#")[0]?.trim() ?? ""; + const isRepoRelative = repoRootPrefixes.some((prefix) => strippedPath.startsWith(prefix)); + + if (!isRepoRelative && (strippedPath.startsWith(".") || !strippedPath.includes("/"))) { + return path.posix.normalize(path.posix.join(path.posix.dirname(truthDocPath), strippedPath)); + } + + return path.posix.normalize(strippedPath); +}; + +const toEvidenceReference = ( + truthDocPath: string, + raw: unknown, +): EvidenceReference | null => { + if (!raw || typeof raw !== "object" || !("path" in raw) || typeof raw.path !== "string") { + return null; + } + + return { + truthDocPath, + path: normalizeReferencePath(truthDocPath, raw.path), + symbol: "symbol" in raw && typeof raw.symbol === "string" ? raw.symbol : undefined, + startLine: "start_line" in raw && typeof raw.start_line === "number" ? raw.start_line : undefined, + endLine: "end_line" in raw && typeof raw.end_line === "number" ? raw.end_line : undefined, + contentHash: + "content_hash" in raw && typeof raw.content_hash === "string" ? raw.content_hash : undefined, + source: "evidence-block", + }; +}; + +export const parseEvidenceReferences = async ( + rootDir: string, + truthDocPath: string, +): Promise => { + const source = await fs.readFile(path.join(rootDir, truthDocPath), "utf8"); + const parsed = matter(source); + const references: EvidenceReference[] = []; + const sourceOfTruth = Array.isArray(parsed.data.source_of_truth) ? parsed.data.source_of_truth : []; + + for (const entry of sourceOfTruth) { + if (typeof entry !== "string") { + continue; + } + + references.push({ + truthDocPath, + path: normalizeReferencePath(truthDocPath, entry), + source: "frontmatter", + }); + } + + for (const match of parsed.content.matchAll(evidenceBlockPattern)) { + const block = parse(match[1] ?? "") as unknown; + const rawEvidence = + block && typeof block === "object" && "evidence" in block + ? (block as { evidence?: unknown }).evidence + : null; + + if (!Array.isArray(rawEvidence)) { + continue; + } + + for (const rawReference of rawEvidence) { + const reference = toEvidenceReference(truthDocPath, rawReference); + if (reference) { + references.push(reference); + } + } + } + + return references; +}; diff --git a/src/evidence/types.ts b/src/evidence/types.ts new file mode 100644 index 0000000..dc62be9 --- /dev/null +++ b/src/evidence/types.ts @@ -0,0 +1,9 @@ +export type EvidenceReference = { + truthDocPath: string; + path: string; + symbol?: string; + startLine?: number; + endLine?: number; + contentHash?: string; + source: "frontmatter" | "evidence-block"; +}; diff --git a/src/evidence/validate.ts b/src/evidence/validate.ts new file mode 100644 index 0000000..70dc213 --- /dev/null +++ b/src/evidence/validate.ts @@ -0,0 +1,170 @@ +import fs from "node:fs/promises"; +import fg from "fast-glob"; + +import type { Diagnostic } from "../output/diagnostic.js"; +import { assertRepoContainment, resolveRepoPath } from "../fs/paths.js"; +import { hashText } from "../markdown/hash.js"; +import { isJavaScriptLikePath } from "../repo-index/file-tree.js"; +import { analyzeTypeScriptSource } from "../repo-index/typescript-symbols.js"; +import { parseEvidenceReferences } from "./parse.js"; +import type { EvidenceReference } from "./types.js"; + +const pathExists = async (filePath: string): Promise => { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +}; + +const diagnosticFor = (reference: EvidenceReference, message: string): Diagnostic => ({ + category: "freshness", + severity: "error", + message, + file: reference.truthDocPath, + data: { + reference: reference.path, + source: reference.source, + }, +}); + +const isGlobReference = (referencePath: string): boolean => /[*?[\]{}()]/u.test(referencePath); + +const validateGlob = async ( + rootDir: string, + reference: EvidenceReference, +): Promise => { + if (reference.path.startsWith("../") || reference.path.startsWith("/")) { + return diagnosticFor(reference, `Referenced file pattern ${reference.path} must stay inside the repository root.`); + } + const matches = await fg(reference.path, { + cwd: rootDir, + dot: true, + onlyFiles: true, + followSymbolicLinks: false, + }); + return matches.length > 0 + ? null + : diagnosticFor(reference, `Referenced file pattern ${reference.path} does not match any file.`); +}; + +const validateSymbol = async ( + rootDir: string, + reference: EvidenceReference, +): Promise => { + if (!reference.symbol || !isJavaScriptLikePath(reference.path)) { + return null; + } + + const source = await fs.readFile(resolveRepoPath(rootDir, reference.path), "utf8"); + const analysis = analyzeTypeScriptSource(reference.path, source); + const hasSymbol = analysis.publicSymbols.some((symbol) => symbol.name === reference.symbol); + + return hasSymbol + ? null + : diagnosticFor(reference, `Evidence symbol ${reference.symbol} was not found in ${reference.path}.`); +}; + +const validateHash = async ( + rootDir: string, + reference: EvidenceReference, +): Promise => { + if (!reference.contentHash) { + return null; + } + if (!reference.contentHash.startsWith("sha256:")) { + return diagnosticFor(reference, `Evidence hash for ${reference.path} must use sha256:.`); + } + + const source = await fs.readFile(resolveRepoPath(rootDir, reference.path), "utf8"); + const lines = source.split("\n"); + const startLine = reference.startLine ?? 1; + const endLine = reference.endLine ?? lines.length; + + if (startLine < 1 || endLine < startLine || endLine > lines.length) { + return diagnosticFor(reference, `Evidence line span for ${reference.path} is outside the file.`); + } + + const actualHash = `sha256:${hashText(lines.slice(startLine - 1, endLine).join("\n"))}`; + + return actualHash === reference.contentHash + ? null + : diagnosticFor(reference, `Evidence hash for ${reference.path} is stale.`); +}; + +const validateLineSpan = async ( + rootDir: string, + reference: EvidenceReference, +): Promise => { + if (reference.startLine === undefined && reference.endLine === undefined) { + return null; + } + + const source = await fs.readFile(resolveRepoPath(rootDir, reference.path), "utf8"); + const lines = source.split("\n"); + const startLine = reference.startLine ?? 1; + const endLine = reference.endLine ?? lines.length; + + return startLine < 1 || endLine < startLine || endLine > lines.length + ? diagnosticFor(reference, `Evidence line span for ${reference.path} is outside the file.`) + : null; +}; + +const validateReference = async ( + rootDir: string, + reference: EvidenceReference, +): Promise => { + const diagnostics: Diagnostic[] = []; + + try { + if (isGlobReference(reference.path)) { + const globDiagnostic = await validateGlob(rootDir, reference); + return globDiagnostic ? [globDiagnostic] : []; + } + + const absolutePath = resolveRepoPath(rootDir, reference.path); + await assertRepoContainment(rootDir, absolutePath); + + if (!(await pathExists(absolutePath))) { + diagnostics.push(diagnosticFor(reference, `Referenced file ${reference.path} does not exist.`)); + return diagnostics; + } + + const symbolDiagnostic = await validateSymbol(rootDir, reference); + if (symbolDiagnostic) { + diagnostics.push(symbolDiagnostic); + } + + const lineSpanDiagnostic = await validateLineSpan(rootDir, reference); + if (lineSpanDiagnostic) { + diagnostics.push(lineSpanDiagnostic); + } else { + const hashDiagnostic = await validateHash(rootDir, reference); + if (hashDiagnostic) { + diagnostics.push(hashDiagnostic); + } + } + } catch { + diagnostics.push(diagnosticFor(reference, `Referenced file ${reference.path} must stay inside the repository root.`)); + } + + return diagnostics; +}; + +export const validateEvidenceReferences = async ( + rootDir: string, + truthDocPaths: string[], +): Promise => { + const diagnostics: Diagnostic[] = []; + + for (const truthDocPath of [...truthDocPaths].sort()) { + const references = await parseEvidenceReferences(rootDir, truthDocPath); + + for (const reference of references) { + diagnostics.push(...(await validateReference(rootDir, reference))); + } + } + + return diagnostics; +}; diff --git a/src/freshness/check.ts b/src/freshness/check.ts new file mode 100644 index 0000000..2d86e93 --- /dev/null +++ b/src/freshness/check.ts @@ -0,0 +1,37 @@ +import type { TruthmarkConfig } from "../config/schema.js"; +import type { Diagnostic } from "../output/diagnostic.js"; +import { buildImpactSet } from "../impact/build.js"; +import type { ImpactSet } from "../impact/types.js"; +import { validateEvidenceReferences } from "../evidence/validate.js"; + +export type FreshnessCheckResult = { + diagnostics: Diagnostic[]; + impactSet: ImpactSet; +}; + +export const checkFreshness = async ( + rootDir: string, + _config: TruthmarkConfig, + truthDocumentPaths: string[], + base: string, +): Promise => { + const impactSet = await buildImpactSet(rootDir, { base }); + const diagnostics: Diagnostic[] = [...(await validateEvidenceReferences(rootDir, truthDocumentPaths))]; + + for (const diagnostic of impactSet.diagnostics) { + if (diagnostic.category !== "impact") { + continue; + } + + diagnostics.push({ + ...diagnostic, + category: "freshness", + message: diagnostic.message.replace("not mapped to a Truthmark route", "not routed to truth ownership"), + }); + } + + return { + diagnostics, + impactSet, + }; +}; diff --git a/src/impact/build.ts b/src/impact/build.ts new file mode 100644 index 0000000..400fd1c --- /dev/null +++ b/src/impact/build.ts @@ -0,0 +1,249 @@ +import path from "node:path"; + +import micromatch from "micromatch"; + +import { loadConfig } from "../config/load.js"; +import { getGitRepository } from "../git/repository.js"; +import type { Diagnostic } from "../output/diagnostic.js"; +import { buildRepoIndex } from "../repo-index/build.js"; +import { isJavaScriptLikePath } from "../repo-index/file-tree.js"; +import { analyzeTypeScriptSource } from "../repo-index/typescript-symbols.js"; +import type { ExportEntry, ImportEdge, RouteMapRoute } from "../repo-index/types.js"; +import { classifyPath } from "../sync/classify.js"; +import { getChangedFiles, readBaseFile } from "./git-diff.js"; +import type { ImpactOptions, ImpactRoute, ImpactSet, PublicSymbolChange } from "./types.js"; + +const uniqueSorted = (values: string[]): string[] => [...new Set(values)].sort(); + +const routeMatchesFile = (route: RouteMapRoute, filePath: string): boolean => { + return route.codeSurface.some((pattern) => micromatch.isMatch(filePath, pattern)); +}; + +const routeOwnsTruthDoc = (route: RouteMapRoute, filePath: string): boolean => { + return route.truthDocs.includes(filePath); +}; + +const toImpactRoute = (route: RouteMapRoute): ImpactRoute => ({ + id: route.id, + name: route.name, + key: route.key, + sourcePath: route.sourcePath, + truthDocs: [...route.truthDocs].sort(), + codeSurface: [...route.codeSurface].sort(), +}); +const changedPathSet = (changedFiles: { path: string; previousPath?: string }[]): Set => { + return new Set( + changedFiles.flatMap((file) => [file.path, ...(file.previousPath ? [file.previousPath] : [])]), + ); +}; + +const changedFilePaths = (changedFile: { path: string; previousPath?: string }): string[] => { + return [changedFile.path, ...(changedFile.previousPath ? [changedFile.previousPath] : [])]; +}; + +const resolveImportPath = (importEdge: ImportEdge): string | null => { + if (!importEdge.specifier.startsWith(".")) { + return null; + } + + const basePath = path.posix.normalize(path.posix.join(path.posix.dirname(importEdge.from), importEdge.specifier)); + const withoutExtension = basePath.replace(/\.[cm]?[jt]sx?$/u, ""); + + return withoutExtension; +}; + +const importTargetsChangedFile = (importEdge: ImportEdge, changedPath: string): boolean => { + const resolved = resolveImportPath(importEdge); + if (!resolved) { + return false; + } + + return changedPath.replace(/\.[cm]?[jt]sx?$/u, "") === resolved; +}; + +const pathSegments = (filePath: string): string[] => filePath.split("/").filter(Boolean); + +const testHintMatchesChangedFile = (hints: string[], changedPath: string): boolean => { + const changedBaseName = path.posix.basename(changedPath); + const changedSegments = pathSegments(changedPath); + + return hints.some((hint) => changedBaseName.startsWith(hint) || changedSegments.includes(hint)); +}; + +const changedSymbolsFor = async ( + cwd: string, + base: string, + basePath: string, + currentPath: string, + currentExports: ExportEntry[], +): Promise => { + if (!isJavaScriptLikePath(basePath) && !isJavaScriptLikePath(currentPath)) { + return []; + } + + const baseSource = await readBaseFile(cwd, base, basePath); + const baseExports = baseSource ? analyzeTypeScriptSource(basePath, baseSource).exports : []; + const currentByName = new Map(currentExports.map((entry) => [entry.name, entry])); + const baseByName = new Map(baseExports.map((entry) => [entry.name, entry])); + const changes: PublicSymbolChange[] = []; + + if (basePath !== currentPath) { + for (const entry of currentExports) { + changes.push({ path: currentPath, name: entry.name, kind: entry.kind, change: "added" }); + } + for (const entry of baseExports) { + changes.push({ path: basePath, name: entry.name, kind: entry.kind, change: "removed" }); + } + return changes; + } + + for (const [name, entry] of currentByName) { + if (!baseByName.has(name)) { + changes.push({ path: currentPath, name, kind: entry.kind, change: "added" }); + } + } + + for (const [name, entry] of baseByName) { + if (!currentByName.has(name)) { + changes.push({ path: basePath, name, kind: entry.kind, change: "removed" }); + } + } + + return changes; +}; + +export const buildImpactSet = async ( + cwd: string, + options: ImpactOptions, +): Promise => { + const repository = await getGitRepository(cwd); + const rootDir = repository.worktreePath; + const [loadResult, repoIndex, changedFilesResult] = await Promise.all([ + loadConfig(rootDir), + buildRepoIndex(rootDir), + getChangedFiles(rootDir, options.base), + ]); + const changedFiles = changedFilesResult.files; + const ignore = loadResult.config?.ignore ?? []; + const diagnostics: Diagnostic[] = [...repoIndex.diagnostics, ...changedFilesResult.diagnostics]; + const affectedRoutes = new Map(); + const affectedTruthDocs: string[] = []; + const affectedTests: string[] = []; + const changedPublicSymbols: PublicSymbolChange[] = []; + const knownTestPaths = new Set(repoIndex.tests.map((test) => test.path)); + + for (const changedFile of changedFiles) { + const routeCandidatePaths = changedFilePaths(changedFile); + if (routeCandidatePaths.some((filePath) => knownTestPaths.has(filePath))) { + affectedTests.push(changedFile.path); + } + const matchingRoutes = repoIndex.routeMap.routes.filter( + (route) => + routeCandidatePaths.some( + (filePath) => routeMatchesFile(route, filePath) || routeOwnsTruthDoc(route, filePath), + ), + ); + + for (const route of matchingRoutes) { + affectedRoutes.set(route.key, toImpactRoute(route)); + affectedTruthDocs.push(...route.truthDocs); + if (route.truthDocs.length === 0) { + diagnostics.push({ + category: "impact", + severity: "review", + message: `Changed file ${changedFile.path} maps to route ${route.name} but the route has no truth document.`, + file: changedFile.path, + area: route.name, + }); + } + } + + if ( + matchingRoutes.length === 0 && + !routeCandidatePaths.some((filePath) => knownTestPaths.has(filePath)) && + routeCandidatePaths.some((filePath) => classifyPath(filePath, ignore) === "functional-code") + ) { + diagnostics.push({ + category: "impact", + severity: "review", + message: `Changed file ${changedFile.path} is not mapped to a Truthmark route.`, + file: changedFile.path, + }); + } + + const currentExports = repoIndex.exports.filter((entry) => entry.path === changedFile.path); + changedPublicSymbols.push( + ...(await changedSymbolsFor( + rootDir, + options.base, + changedFile.previousPath ?? changedFile.path, + changedFile.path, + currentExports, + )), + ); + } + const uniqueAffectedTruthDocs = uniqueSorted(affectedTruthDocs); + const changedPaths = changedPathSet(changedFiles); + for (const symbol of changedPublicSymbols) { + if (uniqueAffectedTruthDocs.length === 0) { + diagnostics.push({ + category: "impact", + severity: "review", + message: `Changed public symbol ${symbol.name} in ${symbol.path} has no affected truth document.`, + file: symbol.path, + data: { + symbol: symbol.name, + change: symbol.change, + }, + }); + continue; + } + if (!uniqueAffectedTruthDocs.some((truthDoc) => changedPaths.has(truthDoc))) { + diagnostics.push({ + category: "impact", + severity: "review", + message: `Changed public symbol ${symbol.name} in ${symbol.path} has affected truth docs but none were changed in this impact set.`, + file: symbol.path, + data: { + symbol: symbol.name, + change: symbol.change, + affectedTruthDocs: uniqueAffectedTruthDocs, + }, + }); + } + } + + for (const test of repoIndex.tests) { + const testImports = repoIndex.imports.filter((edge) => edge.from === test.path); + const importsChangedFile = changedFiles.some((changedFile) => + changedFilePaths(changedFile).some((filePath) => + testImports.some((importEdge) => importTargetsChangedFile(importEdge, filePath)), + ), + ); + const hintMatchesChangedFile = changedFiles.some((changedFile) => + changedFilePaths(changedFile).some((filePath) => + testHintMatchesChangedFile(test.targetHints, filePath), + ), + ); + + if (importsChangedFile || hintMatchesChangedFile) { + affectedTests.push(test.path); + } + } + + return { + schemaVersion: "impact-set/v0", + base: options.base, + headSha: repository.headSha, + changedFiles, + affectedRoutes: [...affectedRoutes.values()].sort((left, right) => + left.key.localeCompare(right.key), + ), + affectedTruthDocs: uniqueAffectedTruthDocs, + affectedTests: uniqueSorted(affectedTests), + changedPublicSymbols: changedPublicSymbols.sort((left, right) => + `${left.path}:${left.name}:${left.change}`.localeCompare(`${right.path}:${right.name}:${right.change}`), + ), + diagnostics, + }; +}; diff --git a/src/impact/git-diff.ts b/src/impact/git-diff.ts new file mode 100644 index 0000000..a44afea --- /dev/null +++ b/src/impact/git-diff.ts @@ -0,0 +1,111 @@ +import { execa } from "execa"; + +import { getUncommittedChanges } from "../git/changes.js"; +import { getGitRepository } from "../git/repository.js"; +import type { Diagnostic } from "../output/diagnostic.js"; +import type { ChangedFileStatus, ImpactFile } from "./types.js"; + +const normalizePath = (filePath: string): string => filePath.replaceAll("\\", "/").replace(/^\.\//u, ""); + +const statusForCode = (code: string): ChangedFileStatus => { + if (code.startsWith("A")) return "added"; + if (code.startsWith("D")) return "deleted"; + if (code.startsWith("R")) return "renamed"; + if (code.startsWith("C")) return "copied"; + if (code.startsWith("T")) return "type-changed"; + return "modified"; +}; + +const mergeFile = (files: Map, next: ImpactFile): void => { + const existing = files.get(next.path); + if (!existing) { + files.set(next.path, next); + return; + } + + files.set(next.path, { + ...existing, + status: + existing.status === "deleted" || existing.status === "renamed" ? existing.status : next.status, + previousPath: existing.previousPath ?? next.previousPath, + staged: existing.staged || next.staged, + unstaged: existing.unstaged || next.unstaged, + untracked: existing.untracked || next.untracked, + deleted: existing.deleted || next.deleted, + }); +}; + +export type ChangedFilesResult = { + files: ImpactFile[]; + diagnostics: Diagnostic[]; +}; +export const getChangedFiles = async (cwd: string, base: string): Promise => { + const repository = await getGitRepository(cwd); + const files = new Map(); + const diagnostics: Diagnostic[] = []; + let diff = await execa("git", ["diff", "--name-status", "--find-renames", `${base}...HEAD`], { + cwd: repository.worktreePath, + reject: false, + }); + + if ((diff.exitCode ?? 1) !== 0) { + diff = await execa("git", ["diff", "--name-status", "--find-renames", base, "HEAD"], { + cwd: repository.worktreePath, + reject: false, + }); + } + + if ((diff.exitCode ?? 1) === 0) { + for (const line of diff.stdout.split("\n").filter(Boolean)) { + const [rawStatus, rawPath, rawNewPath] = line.split("\t"); + const filePath = normalizePath(rawNewPath ?? rawPath); + const previousPath = rawNewPath && rawStatus.startsWith("R") ? normalizePath(rawPath) : undefined; + + mergeFile(files, { + path: filePath, + previousPath, + status: statusForCode(rawStatus), + staged: false, + unstaged: false, + untracked: false, + deleted: rawStatus.startsWith("D"), + }); + } + } else { + diagnostics.push({ + category: "impact", + severity: "error", + message: `Unable to compare base ref ${base} to HEAD.`, + }); + } + + for (const change of await getUncommittedChanges(repository.worktreePath)) { + mergeFile(files, { + path: change.path, + status: change.untracked ? "added" : change.deleted ? "deleted" : "modified", + staged: change.staged, + unstaged: change.unstaged, + untracked: change.untracked, + deleted: change.deleted, + }); + } + + return { + files: [...files.values()].sort((left, right) => left.path.localeCompare(right.path)), + diagnostics, + }; +}; + +export const readBaseFile = async ( + cwd: string, + base: string, + filePath: string, +): Promise => { + const repository = await getGitRepository(cwd); + const result = await execa("git", ["show", `${base}:${filePath}`], { + cwd: repository.worktreePath, + reject: false, + }); + + return (result.exitCode ?? 1) === 0 ? result.stdout : null; +}; diff --git a/src/impact/types.ts b/src/impact/types.ts new file mode 100644 index 0000000..7fa25df --- /dev/null +++ b/src/impact/types.ts @@ -0,0 +1,46 @@ +import type { Diagnostic } from "../output/diagnostic.js"; +import type { ExportKind } from "../repo-index/types.js"; + +export type ChangedFileStatus = "added" | "modified" | "deleted" | "renamed" | "copied" | "type-changed"; + +export type ImpactFile = { + path: string; + previousPath?: string; + status: ChangedFileStatus; + staged: boolean; + unstaged: boolean; + untracked: boolean; + deleted: boolean; +}; + +export type ImpactRoute = { + id: string; + name: string; + key: string; + sourcePath: string; + truthDocs: string[]; + codeSurface: string[]; +}; + +export type PublicSymbolChange = { + path: string; + name: string; + kind: ExportKind; + change: "added" | "removed"; +}; + +export type ImpactSet = { + schemaVersion: "impact-set/v0"; + base: string; + headSha: string | null; + changedFiles: ImpactFile[]; + affectedRoutes: ImpactRoute[]; + affectedTruthDocs: string[]; + affectedTests: string[]; + changedPublicSymbols: PublicSymbolChange[]; + diagnostics: Diagnostic[]; +}; + +export type ImpactOptions = { + base: string; +}; diff --git a/src/init/init.ts b/src/init/init.ts index cd64c18..f1f1433 100644 --- a/src/init/init.ts +++ b/src/init/init.ts @@ -1,60 +1,14 @@ import fs from "node:fs/promises"; import { loadConfig } from "../config/load.js"; -import type { TruthmarkConfig, TruthmarkPlatform } from "../config/schema.js"; +import type { TruthmarkConfig } from "../config/schema.js"; import type { CommandResult, DiagnosticCategory } from "../output/diagnostic.js"; import { getGitRepository } from "../git/repository.js"; import { ensureRepoFile, resolveRepoPath, type FileWriteResult, writeRepoFile } from "../fs/paths.js"; import { detectHierarchyMigrationDiagnostics, scaffoldHierarchy } from "./hierarchy.js"; import { renderAgentsBlock, TRUTHMARK_BLOCK_END, TRUTHMARK_BLOCK_START } from "../templates/agents-block.js"; -import { - renderTruthmarkCopilotCheckPrompt, - renderTruthmarkCopilotDocumentPrompt, - renderTruthmarkCopilotRealizePrompt, - renderTruthmarkCopilotStructurePrompt, - renderTruthmarkCopilotSyncPrompt, - renderTruthmarkCheckLocalSkill, - renderTruthmarkGeminiCheckCommand, - renderTruthmarkGeminiDocumentCommand, - renderTruthmarkGeminiRealizeCommand, - renderTruthmarkGeminiStructureCommand, - renderTruthmarkGeminiSyncCommand, - renderTruthmarkCheckSkill, - renderTruthmarkCheckSkillMetadata, - renderTruthmarkDocumentLocalSkill, - renderTruthmarkDocumentSkill, - renderTruthmarkDocumentSkillMetadata, - renderTruthmarkStructureLocalSkill, - renderTruthmarkStructureSkill, - renderTruthmarkStructureSkillMetadata, - renderTruthmarkSyncLocalSkill, - renderTruthmarkSyncSkill, - renderTruthmarkSyncSkillMetadata, - TRUTHMARK_CHECK_SKILL_METADATA_PATH, - TRUTHMARK_CHECK_SKILL_PATH, - TRUTHMARK_COPILOT_CHECK_PROMPT_PATH, - TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH, - TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH, - TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH, - TRUTHMARK_COPILOT_SYNC_PROMPT_PATH, - TRUTHMARK_DOCUMENT_SKILL_METADATA_PATH, - TRUTHMARK_DOCUMENT_SKILL_PATH, - TRUTHMARK_SYNC_SKILL_METADATA_PATH, - TRUTHMARK_SYNC_SKILL_PATH, - TRUTHMARK_STRUCTURE_SKILL_METADATA_PATH, - TRUTHMARK_STRUCTURE_SKILL_PATH, - renderTruthmarkRealizeLocalSkill, - renderTruthmarkRealizeSkill, - renderTruthmarkRealizeSkillMetadata, - TRUTHMARK_GEMINI_CHECK_COMMAND_PATH, - TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH, - TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH, - TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH, - TRUTHMARK_GEMINI_SYNC_COMMAND_PATH, - TRUTHMARK_REALIZE_SKILL_METADATA_PATH, - TRUTHMARK_REALIZE_SKILL_PATH, -} from "../templates/codex-skills.js"; import { renderDefaultStandards } from "../templates/default-standards.js"; +import { renderGeneratedSurfaces, type GeneratedSurface } from "../templates/generated-surfaces.js"; const escapeRegExp = (value: string): string => { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -275,172 +229,9 @@ const diagnosticCategoryForPath = ( return "config"; }; -type PlatformFile = { - path: string; - content: string; - managedBlock?: boolean; -}; - -const workflowSkillFiles = ( - basePath: string, - config: TruthmarkConfig, -): PlatformFile[] => { - const files: PlatformFile[] = [ - { - path: `${basePath}/truthmark-structure/SKILL.md`, - content: renderTruthmarkStructureLocalSkill(config), - }, - { - path: `${basePath}/truthmark-document/SKILL.md`, - content: renderTruthmarkDocumentLocalSkill(config), - }, - { - path: `${basePath}/truthmark-sync/SKILL.md`, - content: renderTruthmarkSyncLocalSkill(config), - }, - { - path: `${basePath}/truthmark-check/SKILL.md`, - content: renderTruthmarkCheckLocalSkill(config), - }, - { - path: `${basePath}/truthmark-realize/SKILL.md`, - content: renderTruthmarkRealizeLocalSkill(config), - }, - ]; - - return files; -}; - -const codexFiles = (config: TruthmarkConfig): PlatformFile[] => { - const files: PlatformFile[] = [ - { - path: TRUTHMARK_STRUCTURE_SKILL_PATH, - content: renderTruthmarkStructureSkill(config), - }, - { - path: TRUTHMARK_STRUCTURE_SKILL_METADATA_PATH, - content: renderTruthmarkStructureSkillMetadata(), - }, - { - path: TRUTHMARK_DOCUMENT_SKILL_PATH, - content: renderTruthmarkDocumentSkill(config), - }, - { - path: TRUTHMARK_DOCUMENT_SKILL_METADATA_PATH, - content: renderTruthmarkDocumentSkillMetadata(), - }, - { - path: TRUTHMARK_SYNC_SKILL_PATH, - content: renderTruthmarkSyncSkill(config), - }, - { - path: TRUTHMARK_SYNC_SKILL_METADATA_PATH, - content: renderTruthmarkSyncSkillMetadata(), - }, - { - path: TRUTHMARK_CHECK_SKILL_PATH, - content: renderTruthmarkCheckSkill(config), - }, - { - path: TRUTHMARK_CHECK_SKILL_METADATA_PATH, - content: renderTruthmarkCheckSkillMetadata(), - }, - { - path: TRUTHMARK_REALIZE_SKILL_PATH, - content: renderTruthmarkRealizeSkill(config), - }, - { - path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH, - content: renderTruthmarkRealizeSkillMetadata(), - }, - ]; - - return files; -}; - -const copilotFiles = (config: TruthmarkConfig, block: string): PlatformFile[] => { - const files: PlatformFile[] = [ - ...instructionBlockFiles([".github/copilot-instructions.md"], block), - { - path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH, - content: renderTruthmarkCopilotStructurePrompt(config), - }, - { - path: TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH, - content: renderTruthmarkCopilotDocumentPrompt(config), - }, - { - path: TRUTHMARK_COPILOT_SYNC_PROMPT_PATH, - content: renderTruthmarkCopilotSyncPrompt(config), - }, - { - path: TRUTHMARK_COPILOT_CHECK_PROMPT_PATH, - content: renderTruthmarkCopilotCheckPrompt(config), - }, - { - path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH, - content: renderTruthmarkCopilotRealizePrompt(config), - }, - ]; - - return files; -}; - -const instructionBlockFiles = (paths: string[], block: string): PlatformFile[] => { - return paths.map((path) => ({ - path, - content: block, - managedBlock: true, - })); -}; - -const filesForPlatform = ( - platform: TruthmarkPlatform, - config: TruthmarkConfig, - block: string, -): PlatformFile[] => { - switch (platform) { - case "codex": - return codexFiles(config); - case "opencode": - return workflowSkillFiles(".opencode/skills", config); - case "claude-code": - return [ - ...instructionBlockFiles(["CLAUDE.md"], block), - ...workflowSkillFiles(".claude/skills", config), - ]; - case "github-copilot": - return copilotFiles(config, block); - case "gemini-cli": - return [ - ...instructionBlockFiles(["GEMINI.md"], block), - { - path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH, - content: renderTruthmarkGeminiStructureCommand(config), - }, - { - path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH, - content: renderTruthmarkGeminiDocumentCommand(config), - }, - { - path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH, - content: renderTruthmarkGeminiSyncCommand(config), - }, - { - path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH, - content: renderTruthmarkGeminiCheckCommand(config), - }, - { - path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH, - content: renderTruthmarkGeminiRealizeCommand(config), - }, - ]; - } -}; - const writePlatformFile = async ( rootDir: string, - file: PlatformFile, + file: GeneratedSurface, ): Promise => { if (file.managedBlock) { return writeManagedAgentsFile(rootDir, file.path, file.content); @@ -505,15 +296,9 @@ export const runInit = async (cwd: string): Promise => { results.push(...(await scaffoldHierarchy(rootDir, config))); const migrationDiagnostics = await detectHierarchyMigrationDiagnostics(rootDir, config); const block = renderAgentsBlock(config); - const platformFiles = [ - ...instructionBlockFiles(config.instructionTargets, block), - ...config.platforms.flatMap((platform) => filesForPlatform(platform, config, block)), - ]; - const uniquePlatformFiles = Array.from( - new Map(platformFiles.map((file) => [file.path, file])).values(), - ).sort((left, right) => left.path.localeCompare(right.path)); + const platformFiles = renderGeneratedSurfaces(config, block); - for (const file of uniquePlatformFiles) { + for (const file of platformFiles) { results.push(await writePlatformFile(rootDir, file)); } diff --git a/src/output/diagnostic.ts b/src/output/diagnostic.ts index 3009d6d..ad4f5b6 100644 --- a/src/output/diagnostic.ts +++ b/src/output/diagnostic.ts @@ -9,6 +9,10 @@ export const DIAGNOSTIC_CATEGORIES = [ "realization", "doc-structure", "generated-surface", + "repo-index", + "impact", + "freshness", + "context-pack", ] as const; export type DiagnosticCategory = (typeof DIAGNOSTIC_CATEGORIES)[number]; diff --git a/src/repo-index/build.ts b/src/repo-index/build.ts new file mode 100644 index 0000000..64f5de6 --- /dev/null +++ b/src/repo-index/build.ts @@ -0,0 +1,61 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { loadConfig } from "../config/load.js"; +import { getGitRepository } from "../git/repository.js"; +import type { Diagnostic } from "../output/diagnostic.js"; +import { discoverRepoFiles, isJavaScriptLikePath } from "./file-tree.js"; +import { discoverPackageMetadata } from "./package-metadata.js"; +import { buildRouteMap } from "./route-map.js"; +import { analyzeTypeScriptSource } from "./typescript-symbols.js"; +import type { ExportEntry, ImportEdge, PublicSymbolEntry, RepoIndex } from "./types.js"; + +export const buildRepoIndex = async (cwd: string): Promise => { + const repository = await getGitRepository(cwd); + const rootDir = repository.worktreePath; + const loadResult = await loadConfig(rootDir); + const ignore = loadResult.config?.ignore ?? []; + const diagnostics: Diagnostic[] = [...loadResult.diagnostics]; + const [packages, fileTree, routeMap] = await Promise.all([ + discoverPackageMetadata(rootDir), + discoverRepoFiles(rootDir, ignore), + buildRouteMap(rootDir), + ]); + const imports: ImportEdge[] = []; + const exports: ExportEntry[] = []; + const publicSymbols: PublicSymbolEntry[] = []; + + diagnostics.push(...routeMap.diagnostics); + + for (const file of fileTree.files) { + if (!isJavaScriptLikePath(file.path)) { + continue; + } + + const source = await fs.readFile(path.join(rootDir, file.path), "utf8"); + const analysis = analyzeTypeScriptSource(file.path, source); + imports.push(...analysis.imports); + exports.push(...analysis.exports); + publicSymbols.push(...analysis.publicSymbols); + } + + return { + schemaVersion: "repo-index/v0", + repository: { + root: rootDir, + branchName: repository.branchName, + headSha: repository.headSha, + }, + packages, + files: fileTree.files, + docs: fileTree.docs, + tests: fileTree.tests, + imports: imports.sort((left, right) => `${left.from}:${left.specifier}`.localeCompare(`${right.from}:${right.specifier}`)), + exports: exports.sort((left, right) => `${left.path}:${left.name}`.localeCompare(`${right.path}:${right.name}`)), + publicSymbols: publicSymbols.sort((left, right) => + `${left.path}:${left.name}`.localeCompare(`${right.path}:${right.name}`), + ), + routeMap, + diagnostics, + }; +}; diff --git a/src/repo-index/file-tree.ts b/src/repo-index/file-tree.ts new file mode 100644 index 0000000..fef598e --- /dev/null +++ b/src/repo-index/file-tree.ts @@ -0,0 +1,169 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { execa } from "execa"; +import fg from "fast-glob"; +import matter from "gray-matter"; +import micromatch from "micromatch"; + +import { parseMarkdownDocument } from "../markdown/parse.js"; +import { classifyPath } from "../sync/classify.js"; +import type { RepoDocEntry, RepoFileEntry, RepoFileKind, RepoTestEntry } from "./types.js"; + +const languageByExtension = new Map([ + [".ts", "typescript"], + [".tsx", "typescript"], + [".js", "javascript"], + [".jsx", "javascript"], + [".mjs", "javascript"], + [".cjs", "javascript"], + [".md", "markdown"], + [".json", "json"], + [".yml", "yaml"], + [".yaml", "yaml"], + [".toml", "toml"], +]); + +const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]); + +export const isJavaScriptLikePath = (filePath: string): boolean => { + return sourceExtensions.has(path.posix.extname(filePath)); +}; + +const isTestPath = (filePath: string): boolean => { + return ( + filePath.startsWith("tests/") || + filePath.includes("/__tests__/") || + /(?:^|[./-])(test|spec)\.[cm]?[jt]sx?$/u.test(path.posix.basename(filePath)) + ); +}; + +const fileKind = (filePath: string, ignore: string[]): RepoFileKind => { + const classification = classifyPath(filePath, ignore); + if (classification === "derived") { + return "generated"; + } + if (isTestPath(filePath)) { + return "test"; + } + if (filePath.endsWith(".md")) { + return "doc"; + } + if (classification === "functional-code") { + return "source"; + } + if (classification === "markdown") { + return "doc"; + } + if (classification === "config") { + return "config"; + } + + return "other"; +}; + +const targetHintsForTest = (filePath: string): string[] => { + const hints = new Set(); + const basename = path.posix.basename(filePath).replace(/\.(test|spec)\.[cm]?[jt]sx?$/u, ""); + if (basename.length > 0) { + hints.add(basename); + } + + const segments = filePath.split("/"); + const testRootIndex = segments.findIndex((segment) => segment === "tests" || segment === "__tests__"); + if (testRootIndex >= 0) { + for (const segment of segments.slice(testRootIndex + 1, -1)) { + if (segment.length > 0) { + hints.add(segment); + } + } + } + + return [...hints].sort(); +}; + +const defaultIgnore = [".git/**", "node_modules/**", "dist/**", "build/**"]; + +const normalizePath = (filePath: string): string => filePath.replaceAll("\\", "/").replace(/^\.\/+/u, ""); + +const gitDiscoverableFiles = async (rootDir: string): Promise => { + const result = await execa( + "git", + ["ls-files", "--cached", "--others", "--exclude-standard", "--deduplicate"], + { + cwd: rootDir, + reject: false, + }, + ); + if ((result.exitCode ?? 1) !== 0) { + return null; + } + return result.stdout + .split("\n") + .map((line) => normalizePath(line.trim())) + .filter((line) => line.length > 0); +}; + +const isIgnoredPath = (filePath: string, ignore: string[]): boolean => { + return micromatch.isMatch(filePath, [...defaultIgnore, ...ignore]); +}; + +export const discoverRepoFiles = async ( + rootDir: string, + ignore: string[], +): Promise<{ files: RepoFileEntry[]; docs: RepoDocEntry[]; tests: RepoTestEntry[] }> => { + const discoveredFiles = + (await gitDiscoverableFiles(rootDir)) ?? + (await fg(["**/*"], { + cwd: rootDir, + onlyFiles: true, + dot: true, + ignore: [...defaultIgnore, ...ignore], + followSymbolicLinks: false, + })); + const files: RepoFileEntry[] = []; + const docs: RepoDocEntry[] = []; + const tests: RepoTestEntry[] = []; + + for (const filePath of discoveredFiles.filter((entry) => !isIgnoredPath(entry, ignore)).sort()) { + const extension = path.posix.extname(filePath); + const kind = fileKind(filePath, ignore); + + files.push({ + path: filePath, + kind, + language: languageByExtension.get(extension) ?? null, + }); + + if (kind === "test") { + tests.push({ + path: filePath, + targetHints: targetHintsForTest(filePath), + }); + } + + if (kind === "doc") { + const source = await fs.readFile(path.join(rootDir, filePath), "utf8"); + const parsed = matter(source); + const markdown = parseMarkdownDocument(parsed.content); + const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null; + const sourceOfTruth = Array.isArray(parsed.data.source_of_truth) + ? parsed.data.source_of_truth.filter((entry: unknown): entry is string => typeof entry === "string") + : []; + + docs.push({ + path: filePath, + title, + docType: typeof parsed.data.doc_type === "string" ? parsed.data.doc_type : null, + truthKind: typeof parsed.data.truth_kind === "string" ? parsed.data.truth_kind : null, + sourceOfTruth: sourceOfTruth.sort(), + }); + } + } + + return { + files: files.sort((left, right) => left.path.localeCompare(right.path)), + docs: docs.sort((left, right) => left.path.localeCompare(right.path)), + tests: tests.sort((left, right) => left.path.localeCompare(right.path)), + }; +}; diff --git a/src/repo-index/package-metadata.ts b/src/repo-index/package-metadata.ts new file mode 100644 index 0000000..fd97a58 --- /dev/null +++ b/src/repo-index/package-metadata.ts @@ -0,0 +1,63 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import fg from "fast-glob"; + +import type { PackageMetadata } from "./types.js"; + +const packageManagerFor = async ( + rootDir: string, + packageDir: string, +): Promise => { + const lockfiles: Array<[string, PackageMetadata["manager"]]> = [ + ["package-lock.json", "npm"], + ["pnpm-lock.yaml", "pnpm"], + ["yarn.lock", "yarn"], + ["bun.lockb", "bun"], + ["bun.lock", "bun"], + ]; + + for (const [lockfile, manager] of lockfiles) { + try { + await fs.access(path.join(rootDir, packageDir, lockfile)); + return manager; + } catch { + continue; + } + } + + return "npm"; +}; + +export const discoverPackageMetadata = async (rootDir: string): Promise => { + const packageFiles = await fg(["package.json", "*/package.json", "packages/*/package.json"], { + cwd: rootDir, + onlyFiles: true, + ignore: ["node_modules/**", "dist/**", "build/**"], + followSymbolicLinks: false, + }); + const packages: PackageMetadata[] = []; + + for (const packageFile of packageFiles.sort()) { + const packageDir = path.posix.dirname(packageFile) === "." ? "" : path.posix.dirname(packageFile); + const raw = JSON.parse(await fs.readFile(path.join(rootDir, packageFile), "utf8")) as { + name?: unknown; + version?: unknown; + private?: unknown; + scripts?: unknown; + }; + const scripts = + raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : []; + + packages.push({ + path: packageFile, + manager: await packageManagerFor(rootDir, packageDir), + name: typeof raw.name === "string" ? raw.name : null, + version: typeof raw.version === "string" ? raw.version : null, + private: typeof raw.private === "boolean" ? raw.private : null, + scripts, + }); + } + + return packages; +}; diff --git a/src/repo-index/route-map.ts b/src/repo-index/route-map.ts new file mode 100644 index 0000000..db3c43d --- /dev/null +++ b/src/repo-index/route-map.ts @@ -0,0 +1,39 @@ +import { loadConfig } from "../config/load.js"; +import { resolveAreaRouting } from "../routing/area-resolver.js"; +import { resolveTruthDocsRoot } from "../truth/docs.js"; +import type { RouteMap } from "./types.js"; + +export const buildRouteMap = async (rootDir: string): Promise => { + const loadResult = await loadConfig(rootDir); + + if (!loadResult.config) { + return { + schemaVersion: "route-map/v0", + routes: [], + diagnostics: loadResult.diagnostics, + }; + } + + const routing = await resolveAreaRouting(rootDir, { + rootIndex: loadResult.config.docs.routing.rootIndex, + areaFilesRoot: loadResult.config.docs.routing.areaFilesRoot, + truthDocsRoot: resolveTruthDocsRoot(loadResult.config), + }); + + return { + schemaVersion: "route-map/v0", + routes: routing.areas + .map((area) => ({ + id: area.id, + name: area.name, + key: area.key, + sourcePath: area.sourcePath, + parentName: area.parentName, + codeSurface: [...area.codeSurface].sort(), + truthDocs: [...area.truthDocuments].sort(), + updateTruthWhen: [...area.updateTruthWhen], + })) + .sort((left, right) => left.key.localeCompare(right.key)), + diagnostics: routing.diagnostics, + }; +}; diff --git a/src/repo-index/schema.ts b/src/repo-index/schema.ts new file mode 100644 index 0000000..175acb6 --- /dev/null +++ b/src/repo-index/schema.ts @@ -0,0 +1,23 @@ +export const REPO_INDEX_SCHEMA_VERSION = "repo-index/v0" as const; +export const ROUTE_MAP_SCHEMA_VERSION = "route-map/v0" as const; + +export const repoIndexSchema = { + schemaVersion: REPO_INDEX_SCHEMA_VERSION, + required: [ + "schemaVersion", + "repository", + "packages", + "files", + "docs", + "tests", + "imports", + "exports", + "publicSymbols", + "routeMap", + ], +} as const; + +export const routeMapSchema = { + schemaVersion: ROUTE_MAP_SCHEMA_VERSION, + required: ["schemaVersion", "routes"], +} as const; diff --git a/src/repo-index/types.ts b/src/repo-index/types.ts new file mode 100644 index 0000000..51df25d --- /dev/null +++ b/src/repo-index/types.ts @@ -0,0 +1,90 @@ +import type { Diagnostic } from "../output/diagnostic.js"; + +export type PackageMetadata = { + path: string; + manager: "npm" | "pnpm" | "yarn" | "bun" | "unknown"; + name: string | null; + version: string | null; + private: boolean | null; + scripts: string[]; +}; + +export type RepoFileKind = "source" | "test" | "doc" | "config" | "generated" | "other"; + +export type RepoFileEntry = { + path: string; + kind: RepoFileKind; + language: string | null; +}; + +export type RepoDocEntry = { + path: string; + title: string | null; + docType: string | null; + truthKind: string | null; + sourceOfTruth: string[]; +}; + +export type RepoTestEntry = { + path: string; + targetHints: string[]; +}; + +export type ImportEdge = { + from: string; + specifier: string; + imported: string[]; +}; + +export type ExportKind = + | "class" + | "function" + | "const" + | "type" + | "interface" + | "enum" + | "re-export" + | "unknown"; + +export type ExportEntry = { + path: string; + name: string; + kind: ExportKind; +}; + +export type PublicSymbolEntry = ExportEntry; + +export type RouteMapRoute = { + id: string; + name: string; + key: string; + sourcePath: string; + parentName?: string; + codeSurface: string[]; + truthDocs: string[]; + updateTruthWhen: string[]; +}; + +export type RouteMap = { + schemaVersion: "route-map/v0"; + routes: RouteMapRoute[]; + diagnostics: Diagnostic[]; +}; + +export type RepoIndex = { + schemaVersion: "repo-index/v0"; + repository: { + root: string; + branchName: string | null; + headSha: string | null; + }; + packages: PackageMetadata[]; + files: RepoFileEntry[]; + docs: RepoDocEntry[]; + tests: RepoTestEntry[]; + imports: ImportEdge[]; + exports: ExportEntry[]; + publicSymbols: PublicSymbolEntry[]; + routeMap: RouteMap; + diagnostics: Diagnostic[]; +}; diff --git a/src/repo-index/typescript-symbols.ts b/src/repo-index/typescript-symbols.ts new file mode 100644 index 0000000..f3a0163 --- /dev/null +++ b/src/repo-index/typescript-symbols.ts @@ -0,0 +1,124 @@ +import ts from "typescript"; + +import type { ExportEntry, ImportEdge, PublicSymbolEntry } from "./types.js"; + +export type TypeScriptSourceAnalysis = { + imports: ImportEdge[]; + exports: ExportEntry[]; + publicSymbols: PublicSymbolEntry[]; +}; + +const sortStrings = (values: string[]): string[] => [...new Set(values)].sort(); + +const hasExportModifier = (node: ts.Node): boolean => { + return Boolean( + ts.canHaveModifiers(node) && + ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword), + ); +}; + +const declarationName = (node: { name?: ts.PropertyName | ts.BindingName }): string | null => { + if (!node.name || !ts.isIdentifier(node.name)) { + return null; + } + + return node.name.text; +}; + +const addExport = ( + exports: ExportEntry[], + publicSymbols: PublicSymbolEntry[], + path: string, + name: string | null, + kind: ExportEntry["kind"], +): void => { + if (!name) { + return; + } + + const entry = { path, name, kind }; + exports.push(entry); + publicSymbols.push(entry); +}; + +export const analyzeTypeScriptSource = ( + path: string, + source: string, +): TypeScriptSourceAnalysis => { + const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true); + const imports: ImportEdge[] = []; + const exports: ExportEntry[] = []; + const publicSymbols: PublicSymbolEntry[] = []; + + for (const statement of sourceFile.statements) { + if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) { + const imported: string[] = []; + const clause = statement.importClause; + + if (clause?.name) { + imported.push("default"); + } + if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) { + imported.push("*"); + } + if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { + for (const element of clause.namedBindings.elements) { + imported.push(element.propertyName?.text ?? element.name.text); + } + } + + imports.push({ + from: path, + specifier: statement.moduleSpecifier.text, + imported: sortStrings(imported), + }); + continue; + } + + if (ts.isExportDeclaration(statement)) { + if (statement.exportClause && ts.isNamedExports(statement.exportClause)) { + for (const element of statement.exportClause.elements) { + addExport(exports, publicSymbols, path, element.name.text, "re-export"); + } + } + continue; + } + + if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) { + addExport(exports, publicSymbols, path, declarationName(statement), "function"); + continue; + } + + if (ts.isClassDeclaration(statement) && hasExportModifier(statement)) { + addExport(exports, publicSymbols, path, declarationName(statement), "class"); + continue; + } + + if (ts.isInterfaceDeclaration(statement) && hasExportModifier(statement)) { + addExport(exports, publicSymbols, path, declarationName(statement), "interface"); + continue; + } + + if (ts.isTypeAliasDeclaration(statement) && hasExportModifier(statement)) { + addExport(exports, publicSymbols, path, declarationName(statement), "type"); + continue; + } + + if (ts.isEnumDeclaration(statement) && hasExportModifier(statement)) { + addExport(exports, publicSymbols, path, declarationName(statement), "enum"); + continue; + } + + if (ts.isVariableStatement(statement) && hasExportModifier(statement)) { + for (const declaration of statement.declarationList.declarations) { + addExport(exports, publicSymbols, path, declarationName(declaration), "const"); + } + } + } + + return { + imports: imports.sort((left, right) => left.specifier.localeCompare(right.specifier)), + exports: exports.sort((left, right) => left.name.localeCompare(right.name)), + publicSymbols: publicSymbols.sort((left, right) => left.name.localeCompare(right.name)), + }; +}; diff --git a/src/templates/github-action.ts b/src/templates/github-action.ts new file mode 100644 index 0000000..ad538b2 --- /dev/null +++ b/src/templates/github-action.ts @@ -0,0 +1,62 @@ +export const renderGitHubActionExample = (): string => `name: Truthmark Impact + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + truthmark-impact: + runs-on: ubuntu-latest + env: + TRUTHMARK_BLOCKING: "false" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - name: Generate ImpactSet + run: | + npx truthmark impact --base origin/\${{ github.base_ref }} --json > impactset.json + npx truthmark check --base origin/\${{ github.base_ref }} --json > truthmark-check.json + - name: Upload Truthmark reports + uses: actions/upload-artifact@v4 + with: + name: truthmark-impact + path: | + impactset.json + truthmark-check.json + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require("node:fs"); + const impact = JSON.parse(fs.readFileSync("impactset.json", "utf8")); + const check = JSON.parse(fs.readFileSync("truthmark-check.json", "utf8")); + const docs = impact.data.impactSet.affectedTruthDocs; + const body = [ + "## Truthmark Impact", + "", + impact.summary, + check.summary, + "", + "Affected truth docs:", + ...(docs.length === 0 ? ["- none"] : docs.map((path) => \`- \${path}\`)), + ].join("\\n"); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + - name: Enforce blocking mode + if: env.TRUTHMARK_BLOCKING == 'true' + run: | + node -e "const check=require('./truthmark-check.json'); const bad=check.diagnostics.filter((d)=>d.severity==='error'||d.category==='freshness'); if (bad.length) { console.error(JSON.stringify(bad, null, 2)); process.exit(1); }" +`; diff --git a/tests/agents/instructions.test.ts b/tests/agents/instructions.test.ts index 1a1a84a..c43c09c 100644 --- a/tests/agents/instructions.test.ts +++ b/tests/agents/instructions.test.ts @@ -27,6 +27,7 @@ describe("renderTruthSyncInstructions", () => { expect(instructions).toContain("Later functional-code changes reopen the finish-time requirement"); expect(instructions).toContain("must not rewrite functional code"); expect(instructions).toContain("host supports subagent dispatch"); + expect(instructions).toContain("ImpactSet and ContextPack are optional derived context"); expect(instructions).toContain( "If routing is missing, stale, broad, overloaded, catch-all, or cannot map changed code to a bounded truth owner", ); diff --git a/tests/agents/prompts.test.ts b/tests/agents/prompts.test.ts index f7be92f..d6ab83c 100644 --- a/tests/agents/prompts.test.ts +++ b/tests/agents/prompts.test.ts @@ -20,6 +20,8 @@ describe("renderTruthRealizePrompt", () => { expect(prompt).toContain( "implement only bounded, current truth claims from the source docs", ); + expect(prompt).toContain("RepoIndex, RouteMap, ImpactSet, and ContextPack"); + expect(prompt).toContain("workflow write boundaries"); expect(prompt).toContain("write functional code only"); expect(prompt).toContain("do not edit truth docs or truth routing"); expect(prompt).not.toContain("Truth-doc restructure gate"); diff --git a/tests/agents/truth-document.test.ts b/tests/agents/truth-document.test.ts index a701318..98502bf 100644 --- a/tests/agents/truth-document.test.ts +++ b/tests/agents/truth-document.test.ts @@ -87,6 +87,8 @@ describe("renderTruthDocumentSkillBody", () => { expect(skill).toContain( "Repository instruction docs such as docs/ai/repo-rules.md remain instruction authority.", ); + expect(skill).toContain("RepoIndex, RouteMap, ImpactSet, and ContextPack"); + expect(skill).toContain("repository-intelligence artifacts were not generated"); expect(skill).toContain("Evidence Gate"); expect(skill).toContain( "route-first: map the documented behavior to bounded route owners and primary canonical docs", diff --git a/tests/agents/truth-sync.test.ts b/tests/agents/truth-sync.test.ts index ed977b7..e60a9ce 100644 --- a/tests/agents/truth-sync.test.ts +++ b/tests/agents/truth-sync.test.ts @@ -65,6 +65,8 @@ describe("renderTruthSyncSkillBody", () => { expect(skillBody).toContain( "Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries.", ); + expect(skillBody).toContain("RepoIndex, RouteMap, ImpactSet, and ContextPack"); + expect(skillBody).toContain("repository-intelligence artifacts were not generated"); expect(skillBody).not.toContain( "Repository docs and code are inspected evidence, not executable instruction authority.", ); diff --git a/tests/cli/help.test.ts b/tests/cli/help.test.ts index c1b5272..5585ddf 100644 --- a/tests/cli/help.test.ts +++ b/tests/cli/help.test.ts @@ -8,7 +8,6 @@ const forbiddenCommands = [ "scan", "doctor", "build", - "context", "realize", ]; @@ -20,6 +19,9 @@ describe("truthmark CLI", () => { expect(result.stdout).toContain("config"); expect(result.stdout).toContain("init"); expect(result.stdout).toContain("check"); + expect(result.stdout).toContain("index"); + expect(result.stdout).toContain("impact"); + expect(result.stdout).toContain("context"); for (const command of forbiddenCommands) { expect(result.stdout).not.toContain(command); diff --git a/tests/cli/index-impact-context.test.ts b/tests/cli/index-impact-context.test.ts new file mode 100644 index 0000000..f3c7703 --- /dev/null +++ b/tests/cli/index-impact-context.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; + +import { runConfig } from "../../src/config/command.js"; +import { runInit } from "../../src/init/init.js"; +import { runCli } from "../helpers/run-cli.js"; +import { createTempRepo } from "../helpers/temp-repo.js"; + +describe("repository intelligence CLI commands", () => { + it("renders RepoIndex and RouteMap data from truthmark index --json", async () => { + const repo = await createTempRepo(); + try { + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const result = await runCli(["index", "--json"], { cwd: repo.rootDir }); + const output = JSON.parse(result.stdout) as { + command: string; + diagnostics: unknown[]; + data: { + repoIndex: { schemaVersion: string }; + routeMap: { schemaVersion: string }; + }; + }; + + expect(output.command).toBe("index"); + expect(output.data.repoIndex.schemaVersion).toBe("repo-index/v0"); + expect(output.data.routeMap.schemaVersion).toBe("route-map/v0"); + expect(output.diagnostics).toEqual([]); + } finally { + await repo.cleanup(); + } + }); + + it("renders ImpactSet from truthmark impact --base main --json", async () => { + const repo = await createTempRepo(); + try { + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const value = 2;\n"); + + const result = await runCli(["impact", "--base", "main", "--json"], { cwd: repo.rootDir }); + const output = JSON.parse(result.stdout) as { + command: string; + data: { impactSet: { schemaVersion: string; changedFiles: Array<{ path: string }> } }; + }; + + expect(output.command).toBe("impact"); + expect(output.data.impactSet.schemaVersion).toBe("impact-set/v0"); + expect(output.data.impactSet.changedFiles.map((file) => file.path)).toContain("src/index.ts"); + } finally { + await repo.cleanup(); + } + }); + + it("renders ContextPack from truthmark context --workflow truth-sync --base main --json", async () => { + const repo = await createTempRepo(); + try { + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const value = 2;\n"); + + const result = await runCli(["context", "--workflow", "truth-sync", "--base", "main", "--json"], { + cwd: repo.rootDir, + }); + const output = JSON.parse(result.stdout) as { + command: string; + data: { contextPack: { schemaVersion: string; workflow: string } }; + }; + + expect(output.command).toBe("context"); + expect(output.data.contextPack.schemaVersion).toBe("context-pack/v0"); + expect(output.data.contextPack.workflow).toBe("truth-sync"); + } finally { + await repo.cleanup(); + } + }); + + it("renders ContextPack markdown when --format markdown is requested", async () => { + const repo = await createTempRepo(); + try { + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const result = await runCli(["context", "--workflow", "truth-document", "--format", "markdown"], { + cwd: repo.rootDir, + }); + + expect(result.stdout).toContain("# Truthmark ContextPack (truth-document)"); + expect(result.stdout).toContain("## Allowed Write Paths"); + } finally { + await repo.cleanup(); + } + }); + + it("rejects unsupported ContextPack formats", async () => { + const repo = await createTempRepo(); + try { + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const result = await runCli(["context", "--workflow", "truth-sync", "--format", "typo", "--json"], { + cwd: repo.rootDir, + }); + const output = JSON.parse(result.stdout) as { + diagnostics: Array<{ category: string; severity: string; message: string }>; + }; + + expect(output.diagnostics).toContainEqual( + expect.objectContaining({ + category: "context-pack", + severity: "error", + message: expect.stringContaining("--format json or markdown"), + }), + ); + } finally { + await repo.cleanup(); + } + }); +}); diff --git a/tests/config/load.test.ts b/tests/config/load.test.ts index 25edf06..ebb93d8 100644 --- a/tests/config/load.test.ts +++ b/tests/config/load.test.ts @@ -22,7 +22,13 @@ authority: expect(result.diagnostics).toEqual([]); expect(result.config).toMatchObject({ version: 1, - platforms: ["codex", "opencode", "claude-code", "github-copilot", "gemini-cli"], + platforms: [ + "codex", + "opencode", + "claude-code", + "github-copilot", + "gemini-cli", + ], authority: ["docs/truthmark/areas.md"], docs: { layout: "hierarchical", @@ -223,7 +229,6 @@ authority: await repo.cleanup(); } }); - it("returns config diagnostics for invalid config data", async () => { const repo = await createTempRepo(); diff --git a/tests/context-pack/build.test.ts b/tests/context-pack/build.test.ts new file mode 100644 index 0000000..bf0ba8a --- /dev/null +++ b/tests/context-pack/build.test.ts @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { buildContextPack } from "../../src/context-pack/build.js"; +import { runConfig } from "../../src/config/command.js"; +import { runInit } from "../../src/init/init.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("buildContextPack", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("builds a bounded Truth Sync context pack from an impact set", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const value = 2;\n"); + + const pack = await buildContextPack(repo.rootDir, { workflow: "truth-sync", base: "main" }); + + expect(pack.schemaVersion).toBe("context-pack/v0"); + expect(pack.workflow).toBe("truth-sync"); + expect(pack.allowedWritePaths).toContain("docs/truthmark/areas.md"); + expect(pack.truthDocs.length).toBeGreaterThan(0); + expect(pack.sourceFiles.map((file) => file.path)).toContain("src/index.ts"); + }); + + it("bounds Truth Realize writes to matched code surfaces", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const value = 2;\n"); + + const pack = await buildContextPack(repo.rootDir, { workflow: "truth-realize", base: "main" }); + + expect(pack.allowedWritePaths).toContain("src/**"); + }); + + it("does not widen no-base Truth Realize context packs to every code surface", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/owned/index.ts", "export const owned = 1;\n"); + await repo.writeFile("src/other/index.ts", "export const other = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.writeFile( + "docs/truthmark/areas.md", + `# Truthmark Areas + +## Owned + +Truth documents: +- docs/truth/owned.md + +Code surface: +- src/owned/** + +Update truth when: +- owned behavior changes + +## Other + +Truth documents: +- docs/truth/other.md + +Code surface: +- src/other/** + +Update truth when: +- other behavior changes +`, + ); + await repo.writeFile( + "docs/truth/owned.md", + `--- +source_of_truth: + - ../../src/owned/index.ts +--- +# Owned +`, + ); + await repo.writeFile( + "docs/truth/other.md", + `--- +source_of_truth: + - ../../src/other/index.ts +--- +# Other +`, + ); + + const pack = await buildContextPack(repo.rootDir, { workflow: "truth-realize" }); + + expect(pack.allowedWritePaths).toEqual([]); + expect(pack.truthDocs).toEqual([]); + expect(pack.sourceFiles).toEqual([]); + expect(pack.warnings).toContainEqual( + expect.objectContaining({ + category: "context-pack", + severity: "review", + message: expect.stringContaining("truth-realize requires --base"), + }), + ); + }); + + it("includes source_of_truth files for Truth Document context without a base ref", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.writeFile( + "docs/truth/repository/overview.md", + `--- +status: active +doc_type: behavior +truth_kind: behavior +source_of_truth: + - ../../../src/index.ts +--- +# Repository Overview +`, + ); + + const pack = await buildContextPack(repo.rootDir, { workflow: "truth-document" }); + + expect(pack.sourceFiles.map((file) => file.path)).toContain("src/index.ts"); + }); +}); diff --git a/tests/evidence/validate.test.ts b/tests/evidence/validate.test.ts new file mode 100644 index 0000000..f2b6dd5 --- /dev/null +++ b/tests/evidence/validate.test.ts @@ -0,0 +1,94 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { validateEvidenceReferences } from "../../src/evidence/validate.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("validateEvidenceReferences", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("reports deleted referenced files", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile( + "docs/truth/sample.md", + "---\nstatus: active\nsource_of_truth:\n - ../../src/missing.ts\n---\n# Sample\n", + ); + + const diagnostics = await validateEvidenceReferences(repo.rootDir, ["docs/truth/sample.md"]); + + expect(diagnostics).toContainEqual( + expect.objectContaining({ + category: "freshness", + severity: "error", + file: "docs/truth/sample.md", + }), + ); + }); + + it("accepts source_of_truth glob references that match repository files", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await repo.writeFile( + "docs/truth/sample.md", + "---\nstatus: active\nsource_of_truth:\n - ../../src/**/*.ts\n---\n# Sample\n", + ); + + const diagnostics = await validateEvidenceReferences(repo.rootDir, ["docs/truth/sample.md"]); + + expect(diagnostics).toEqual([]); + }); + + it("resolves bare source_of_truth filenames relative to the truth doc", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("docs/truth/overview.md", "# Overview\n"); + await repo.writeFile( + "docs/truth/sample.md", + "---\nstatus: active\nsource_of_truth:\n - overview.md\n---\n# Sample\n", + ); + + const diagnostics = await validateEvidenceReferences(repo.rootDir, ["docs/truth/sample.md"]); + + expect(diagnostics).toEqual([]); + }); + + it("reports evidence line spans outside the file even without a content hash", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await repo.writeFile( + "docs/truth/sample.md", + `--- +status: active +--- +# Sample + +\`\`\`yaml +evidence: + - path: ../../src/index.ts + start_line: 10 + end_line: 12 +\`\`\` +`, + ); + + const diagnostics = await validateEvidenceReferences(repo.rootDir, ["docs/truth/sample.md"]); + + expect(diagnostics).toContainEqual( + expect.objectContaining({ + category: "freshness", + severity: "error", + file: "docs/truth/sample.md", + message: expect.stringContaining("outside the file"), + }), + ); + }); +}); diff --git a/tests/freshness/check.test.ts b/tests/freshness/check.test.ts new file mode 100644 index 0000000..9dba844 --- /dev/null +++ b/tests/freshness/check.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { runCheck } from "../../src/checks/check.js"; +import { runConfig } from "../../src/config/command.js"; +import { runInit } from "../../src/init/init.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("freshness diagnostics", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("reports changed code with no routed truth document when a base ref is supplied", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("scripts/unmapped.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("scripts/unmapped.ts", "export const value = 2;\n"); + + const result = await runCheck(repo.rootDir, { base: "main" }); + + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + category: "freshness", + severity: "review", + file: "scripts/unmapped.ts", + }), + ); + }); + + it("reports changed API when affected truth docs were not changed", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const changedValue = 2;\n"); + + const result = await runCheck(repo.rootDir, { base: "main" }); + + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + category: "freshness", + severity: "review", + file: "src/index.ts", + message: expect.stringContaining("affected truth docs but none were changed"), + }), + ); + }); +}); diff --git a/tests/impact/build.test.ts b/tests/impact/build.test.ts new file mode 100644 index 0000000..69b9b53 --- /dev/null +++ b/tests/impact/build.test.ts @@ -0,0 +1,210 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { runConfig } from "../../src/config/command.js"; +import { buildImpactSet } from "../../src/impact/build.js"; +import { runInit } from "../../src/init/init.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("buildImpactSet", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("maps changed files to routes, truth docs, and tests", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/math.ts", "export function add(left: number, right: number) { return left + right; }\n"); + await repo.writeFile("tests/math.test.ts", "import { add } from '../src/math.js';\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + + await repo.writeFile( + "src/math.ts", + "export function add(left: number, right: number) { return left + right + 0; }\n", + ); + + const impact = await buildImpactSet(repo.rootDir, { base: "main" }); + + expect(impact.schemaVersion).toBe("impact-set/v0"); + expect(impact.changedFiles).toContainEqual(expect.objectContaining({ path: "src/math.ts", status: "modified" })); + expect(impact.affectedTruthDocs.length).toBeGreaterThan(0); + expect(impact.affectedTests).toContain("tests/math.test.ts"); + }); + + it("maps changed routed truth docs to affected routes and truth docs", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + + const truthDocPath = "docs/truth/repository/overview.md"; + await repo.writeFile( + truthDocPath, + `${await repo.readFile(truthDocPath)}\nUpdated direct truth-doc edit.\n`, + ); + + const impact = await buildImpactSet(repo.rootDir, { base: "main" }); + + expect(impact.changedFiles).toContainEqual( + expect.objectContaining({ path: truthDocPath, status: "modified" }), + ); + expect(impact.affectedTruthDocs).toContain(truthDocPath); + expect(impact.affectedRoutes.some((route) => route.truthDocs.includes(truthDocPath))).toBe( + true, + ); + }); + + it("reports when a base ref cannot be compared", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + + const impact = await buildImpactSet(repo.rootDir, { base: "missing-ref" }); + + expect(impact.diagnostics).toContainEqual( + expect.objectContaining({ + category: "impact", + severity: "error", + message: expect.stringContaining("Unable to compare base ref missing-ref"), + }), + ); + }); + + it("reports public API changes when affected truth docs were not changed", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("src/index.ts", "export const changedValue = 2;\n"); + + const impact = await buildImpactSet(repo.rootDir, { base: "main" }); + + expect(impact.diagnostics).toContainEqual( + expect.objectContaining({ + category: "impact", + severity: "review", + file: "src/index.ts", + message: expect.stringContaining("affected truth docs but none were changed"), + }), + ); + }); + + it("treats changed test files as affected tests instead of unrouted code", async () => { + const repo = await createTempRepo(); + repos.push(repo); + + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await repo.writeFile("tests/index.test.ts", "import { value } from '../src/index.js';\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.writeFile("tests/index.test.ts", "import { value } from '../src/index.js';\nvoid value;\n"); + + const impact = await buildImpactSet(repo.rootDir, { base: "main" }); + + expect(impact.affectedTests).toContain("tests/index.test.ts"); + expect(impact.diagnostics).not.toContainEqual( + expect.objectContaining({ + category: "impact", + file: "tests/index.test.ts", + }), + ); + }); + + it("selects package-level tests for changed package files", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/repo-index/package-metadata.ts", "export const manager = 'npm';\n"); + await repo.writeFile( + "tests/repo-index/build.test.ts", + "import { describe, it } from 'vitest';\ndescribe('repo index package', () => { it('builds', () => undefined); });\n", + ); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + + await repo.writeFile( + "src/repo-index/package-metadata.ts", + "export const manager = 'pnpm';\n", + ); + + const impact = await buildImpactSet(repo.rootDir, { base: "main" }); + + expect(impact.affectedTests).toContain("tests/repo-index/build.test.ts"); + }); + + it("keeps old and new route ownership for renamed files", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/old/api.ts", "export const oldApi = 1;\n"); + await repo.writeFile("src/new/.gitkeep", "\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + await repo.writeFile( + "docs/truthmark/areas.md", + `# Truthmark Areas + +## Old + +Truth documents: +- docs/truth/old.md + +Code surface: +- src/old/** + +Update truth when: +- old behavior changes + +## New + +Truth documents: +- docs/truth/new.md + +Code surface: +- src/new/** + +Update truth when: +- new behavior changes +`, + ); + await repo.writeFile("docs/truth/old.md", "# Old\n"); + await repo.writeFile("docs/truth/new.md", "# New\n"); + await repo.runGit(["add", "."]); + await repo.runGit(["commit", "-m", "initial"]); + await repo.runGit(["branch", "baseline"]); + await repo.runGit(["mv", "src/old/api.ts", "src/new/api.ts"]); + await repo.runGit(["commit", "-m", "move api"]); + + const impact = await buildImpactSet(repo.rootDir, { base: "baseline" }); + + expect(impact.changedFiles).toContainEqual( + expect.objectContaining({ + path: "src/new/api.ts", + previousPath: "src/old/api.ts", + status: "renamed", + }), + ); + expect(impact.affectedTruthDocs).toEqual(["docs/truth/new.md", "docs/truth/old.md"]); + expect(impact.changedPublicSymbols).toContainEqual({ + path: "src/old/api.ts", + name: "oldApi", + kind: "const", + change: "removed", + }); + }); +}); diff --git a/tests/init/init.test.ts b/tests/init/init.test.ts index 1c41eaf..38f8e12 100644 --- a/tests/init/init.test.ts +++ b/tests/init/init.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; +import { getTruthmarkWorkflow } from "../../src/agents/workflow-manifest.js"; import { runCheck } from "../../src/checks/check.js"; import { runConfig } from "../../src/config/command.js"; import { runInit } from "../../src/init/init.js"; @@ -502,7 +503,7 @@ Agent-specific: expect( await repo.readFile(".gemini/commands/truthmark/sync.toml"), ).toContain( - 'description = "Sync repository truth docs from functional code changes; skip docs-only/no-code changes."', + `description = "${getTruthmarkWorkflow("truthmark-sync").description}"`, ); expect( await repo.readFile(".gemini/commands/truthmark/sync.toml"), @@ -512,7 +513,9 @@ Agent-specific: ).toContain("name: truthmark-document"); expect( await repo.readFile(".gemini/commands/truthmark/realize.toml"), - ).toContain('description = "Realize repository truth docs into code."'); + ).toContain( + `description = "${getTruthmarkWorkflow("truthmark-realize").description}"`, + ); const geminiInstructions = await repo.readFile("GEMINI.md"); expect(geminiInstructions).not.toContain("/truthmark:sync"); expect(geminiInstructions).toContain( diff --git a/tests/output/render.test.ts b/tests/output/render.test.ts index f663839..69d3a28 100644 --- a/tests/output/render.test.ts +++ b/tests/output/render.test.ts @@ -99,6 +99,10 @@ describe("output rendering", () => { "realization", "doc-structure", "generated-surface", + "repo-index", + "impact", + "freshness", + "context-pack", ]); }); }); diff --git a/tests/repo-index/build.test.ts b/tests/repo-index/build.test.ts new file mode 100644 index 0000000..1fe8b44 --- /dev/null +++ b/tests/repo-index/build.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { runConfig } from "../../src/config/command.js"; +import { runInit } from "../../src/init/init.js"; +import { buildRepoIndex } from "../../src/repo-index/build.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("buildRepoIndex", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("indexes package metadata, files, docs, tests, exports, symbols, and route ownership", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile( + "package.json", + JSON.stringify({ name: "sample", version: "1.0.0", scripts: { test: "vitest" } }, null, 2), + ); + await repo.writeFile( + "src/math.ts", + "export function add(left: number, right: number) { return left + right; }\n", + ); + await repo.writeFile("src/index.ts", "export { add } from './math.js';\n"); + await repo.writeFile("tests/math.test.ts", "import { add } from '../src/math.js';\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const result = await buildRepoIndex(repo.rootDir); + + expect(result.schemaVersion).toBe("repo-index/v0"); + expect(result.packages).toContainEqual( + expect.objectContaining({ name: "sample", version: "1.0.0", manager: "npm" }), + ); + expect(result.files.map((file) => file.path)).toContain("src/math.ts"); + expect(result.docs.map((doc) => doc.path)).toContain("docs/truth/repository/overview.md"); + expect(result.files).toContainEqual( + expect.objectContaining({ path: "AGENTS.md", kind: "generated" }), + ); + expect(result.docs.map((doc) => doc.path)).not.toContain("AGENTS.md"); + expect(result.tests.map((file) => file.path)).toContain("tests/math.test.ts"); + expect(result.exports).toContainEqual({ path: "src/math.ts", name: "add", kind: "function" }); + expect(result.publicSymbols).toContainEqual({ path: "src/math.ts", name: "add", kind: "function" }); + expect(result.routeMap.routes.some((route) => route.truthDocs.length > 0)).toBe(true); + }); + + it("excludes files ignored by gitignore", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await repo.writeFile(".gitignore", ".lean-ctx/\n"); + await repo.writeFile(".lean-ctx/graph.meta.json", "{}\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const result = await buildRepoIndex(repo.rootDir); + + expect(result.files.map((file) => file.path)).not.toContain(".lean-ctx/graph.meta.json"); + }); +}); diff --git a/tests/repo-index/route-map.test.ts b/tests/repo-index/route-map.test.ts new file mode 100644 index 0000000..7e7b988 --- /dev/null +++ b/tests/repo-index/route-map.test.ts @@ -0,0 +1,34 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { runConfig } from "../../src/config/command.js"; +import { runInit } from "../../src/init/init.js"; +import { buildRouteMap } from "../../src/repo-index/route-map.js"; +import { createTempRepo, type TempRepo } from "../helpers/temp-repo.js"; + +describe("buildRouteMap", () => { + const repos: TempRepo[] = []; + + afterEach(async () => { + await Promise.all(repos.splice(0).map((repo) => repo.cleanup())); + }); + + it("projects Truthmark area routing into route-map/v0", async () => { + const repo = await createTempRepo(); + repos.push(repo); + await repo.writeFile("src/index.ts", "export const value = 1;\n"); + await runConfig(repo.rootDir, { force: false, stdout: false }); + await runInit(repo.rootDir); + + const routeMap = await buildRouteMap(repo.rootDir); + + expect(routeMap.schemaVersion).toBe("route-map/v0"); + expect(routeMap.routes.length).toBeGreaterThan(0); + expect(routeMap.routes[0]).toEqual( + expect.objectContaining({ + name: expect.any(String), + codeSurface: expect.any(Array), + truthDocs: expect.any(Array), + }), + ); + }); +}); diff --git a/tests/repo-index/typescript-symbols.test.ts b/tests/repo-index/typescript-symbols.test.ts new file mode 100644 index 0000000..1eb2c48 --- /dev/null +++ b/tests/repo-index/typescript-symbols.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { analyzeTypeScriptSource } from "../../src/repo-index/typescript-symbols.js"; + +describe("analyzeTypeScriptSource", () => { + it("extracts imports, exports, and public symbols from TypeScript source", () => { + const result = analyzeTypeScriptSource( + "src/service.ts", + [ + "import defaultThing, { readFile as read } from 'node:fs/promises';", + "import * as pathTools from 'node:path';", + "export class Service {}", + "export const value = 1;", + "export function run() { return value; }", + "export interface Options { enabled: boolean }", + "export type Mode = 'auto';", + "export enum State { Ready }", + "export { helper } from './helper.js';", + ].join("\n"), + ); + + expect(result.imports).toContainEqual({ + from: "src/service.ts", + specifier: "node:fs/promises", + imported: ["default", "readFile"], + }); + expect(result.imports).toContainEqual({ + from: "src/service.ts", + specifier: "node:path", + imported: ["*"], + }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "Service", kind: "class" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "value", kind: "const" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "run", kind: "function" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "Options", kind: "interface" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "Mode", kind: "type" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "State", kind: "enum" }); + expect(result.exports).toContainEqual({ path: "src/service.ts", name: "helper", kind: "re-export" }); + expect(result.publicSymbols).toContainEqual({ path: "src/service.ts", name: "run", kind: "function" }); + }); +}); diff --git a/tests/templates/github-action.test.ts b/tests/templates/github-action.test.ts new file mode 100644 index 0000000..e274447 --- /dev/null +++ b/tests/templates/github-action.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { renderGitHubActionExample } from "../../src/templates/github-action.js"; + +describe("renderGitHubActionExample", () => { + it("documents non-blocking and blocking Truthmark impact modes", () => { + const content = renderGitHubActionExample(); + + expect(content).toContain("truthmark impact --base"); + expect(content).toContain("truthmark check --base"); + expect(content).toContain("TRUTHMARK_BLOCKING"); + expect(content).toContain("github-script"); + }); +});