Files
truthmark/src/sync/classify.ts
T
a083b703fa feat: add Truthmark health scorecard and bounded workflow contracts (#13)
* feat: add OpenSpec-driven workflow state

* docs: plan generated playbooks workflow contract

* feat: add truth health scorecard

Release Truthmark 2.1.0 with check JSON scorecard output.

Archive the completed OpenSpec pass 4 scorecard change and update generated truth surfaces.

* fix: unblock truthmark scorecard review

- restore and update coverage for scorecard/evidence/workflow behavior
- make truthmark-sync select a cheap local base when --base is omitted
- report malformed evidence YAML blocks as diagnostics instead of throwing

* fix: tighten workflow boundaries

* fix: fail closed when sync has no comparison base

* fix: tighten truth health review coverage

- only parse fenced YAML evidence blocks with top-level evidence markers
- convert coverage fixtures to valid v2 config and assert non-vacuous diagnostics
- update Truthmark truth docs for source traceability and workflow-state tests

* docs: clarify source traceability diagnostics

* docs: harden optional CLI workflow boundary

* docs: clarify product boundary is repo-local

* fix: bound truthmark workflow fallback behavior

- make live workflow preflight use the one-call instructions contract
- keep missing-CLI fallback bounded for Sync, Document, Structure, and Realize
- add routing and generated-surface coverage plus refreshed host surfaces

* test: cover no-cli workflow fallback bounds

- assert route-first fallback wording in generated workflow surfaces
- clarify progressive-disclosure support files are conditional
- align overview docs with bounded no-CLI behavior

* docs: route workflow eval coverage

* fix: remove workflow instructions preflight

* fix: remove stale workflow preflight wording

* fix: remove generic workflow validation blocks

* fix: avoid duplicated markdown context JSON

* fix: keep workflow status manifest-only

* docs: align truth docs with v2 hierarchy

---------

Co-authored-by: MerlinH <merlinh221@gmail.com>
2026-06-13 04:09:45 +10:00

207 lines
4.7 KiB
TypeScript

import micromatch from "micromatch";
export type PathClassification =
| "functional-code"
| "markdown"
| "config"
| "ignored"
| "derived"
| "other";
const CODE_EXTENSIONS = new Set([
".c",
".cc",
".cpp",
".cs",
".cts",
".cjs",
".ex",
".exs",
".gql",
".go",
".graphql",
".h",
".hpp",
".hrl",
".java",
".js",
".jsx",
".kt",
".kts",
".lua",
".mjs",
".mts",
".php",
".proto",
".py",
".rb",
".rs",
".scala",
".sh",
".swift",
".tf",
".tfvars",
".ts",
".tsx",
]);
const CONFIG_EXTENSIONS = new Set([
".cfg",
".conf",
".env",
".ini",
".json",
".jsonc",
".toml",
".yaml",
".yml",
]);
const CONFIG_BASENAMES = new Set([
".editorconfig",
".gitattributes",
".gitignore",
"Dockerfile",
"package-lock.json",
"package.json",
"pnpm-lock.yaml",
"tsconfig.json",
"yarn.lock",
]);
const CONFIG_SUFFIXES = [
".config.cjs",
".config.js",
".config.mjs",
".config.ts",
".config.tsx",
".config.jsx",
];
const COMMON_CODE_DIRECTORIES =
/(^|\/)(api|app|apps|bin|client|cmd|components|frontend|infra|infrastructure|k8s|kubernetes|lib|packages|proto|schema|schemas|scripts|server|services|src|terraform|web)\//u;
const FUNCTIONAL_CONFIG_DIRECTORIES =
/(^|\/)(api|infra|infrastructure|k8s|kubernetes|schema|schemas|terraform)\//u;
const FUNCTIONAL_CONFIG_BASENAMES = new Set([
"openapi.json",
"openapi.yaml",
"openapi.yml",
"swagger.json",
"swagger.yaml",
"swagger.yml",
]);
const normalizePath = (filePath: string): string => {
return filePath.replaceAll("\\", "/").replace(/^\.\//u, "");
};
const getBaseName = (filePath: string): string => {
const segments = normalizePath(filePath).split("/");
return segments.at(-1) ?? filePath;
};
const getExtension = (filePath: string): string => {
const baseName = getBaseName(filePath);
const extensionIndex = baseName.lastIndexOf(".");
if (extensionIndex <= 0) {
return "";
}
return baseName.slice(extensionIndex).toLowerCase();
};
const isConfigPath = (filePath: string): boolean => {
const normalizedPath = normalizePath(filePath);
const baseName = getBaseName(normalizedPath);
const extension = getExtension(normalizedPath);
return (
CONFIG_BASENAMES.has(baseName) ||
CONFIG_EXTENSIONS.has(extension) ||
CONFIG_SUFFIXES.some((suffix) => baseName.endsWith(suffix))
);
};
const isFunctionalConfigPath = (filePath: string): boolean => {
const normalizedPath = normalizePath(filePath);
const baseName = getBaseName(normalizedPath).toLowerCase();
const extension = getExtension(normalizedPath);
return (
normalizedPath.startsWith(".github/workflows/") ||
FUNCTIONAL_CONFIG_BASENAMES.has(baseName) ||
((extension === ".yaml" || extension === ".yml" || extension === ".json") &&
FUNCTIONAL_CONFIG_DIRECTORIES.test(normalizedPath))
);
};
const isCodeLikePath = (filePath: string): boolean => {
const normalizedPath = normalizePath(filePath);
const extension = getExtension(normalizedPath);
if (CODE_EXTENSIONS.has(extension)) {
return true;
}
return extension.length === 0 && COMMON_CODE_DIRECTORIES.test(normalizedPath);
};
export const classifyPath = (
filePath: string,
ignorePatterns: string[],
): PathClassification => {
const normalizedPath = normalizePath(filePath);
if (normalizedPath === ".truthmark/config.yml") {
return "config";
}
if (normalizedPath.startsWith(".truthmark/")) {
return "derived";
}
if (
normalizedPath.startsWith(".agents/skills/truthmark-") ||
normalizedPath.startsWith(".claude/") ||
normalizedPath.startsWith(".codex/") ||
normalizedPath.startsWith(".gemini/agents/truth-") ||
normalizedPath.startsWith(".gemini/commands/") ||
normalizedPath.startsWith(".gemini/skills/truthmark-") ||
normalizedPath.startsWith(".opencode/") ||
normalizedPath === ".github/copilot-instructions.md" ||
normalizedPath.startsWith(".github/agents/truth-") ||
normalizedPath.startsWith(".github/prompts/truthmark-") ||
normalizedPath.startsWith(".github/skills/truthmark-") ||
normalizedPath === "AGENTS.md" ||
normalizedPath === "CLAUDE.md" ||
normalizedPath === "GEMINI.md" ||
normalizedPath.startsWith(".gemini/commands/truthmark/")
) {
return "derived";
}
if (ignorePatterns.length > 0 && micromatch.isMatch(normalizedPath, ignorePatterns)) {
return "ignored";
}
if (normalizedPath.toLowerCase().endsWith(".md")) {
return "markdown";
}
if (isFunctionalConfigPath(normalizedPath)) {
return "functional-code";
}
if (isConfigPath(normalizedPath)) {
return "config";
}
if (isCodeLikePath(normalizedPath)) {
return "functional-code";
}
return "other";
};