@blundergoat/goat-flow
Version:
AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.
163 lines • 7.1 kB
JavaScript
import { PROJECT_STACK_CODE_GENERATION_SIGNALS, PROJECT_STACK_COMPLIANCE_DOCS, PROJECT_STACK_DEPLOYMENT_SIGNALS, PROJECT_STACK_FORMATTER_MAP, PROJECT_STACK_LLM_DEPENDENCY_FILES, PROJECT_STACK_LLM_ENV_FILES, } from "./project-stack-data.js";
import { hasAnyGlob, hasAnyPath } from "./project-stack-files.js";
/**
* Count distinct source files under the conventional code roots, used as a coarse
* project-size signal. Globs only src/lib/app/packages, so generated, vendor, and
* build output outside those trees is excluded by construction rather than filtered.
*
* @param fs - read-only filesystem adapter for the target project
* @returns the de-duplicated file count across the code roots; 0 when none match
*/
export function countSourceFiles(fs) {
const patterns = [
"src/**/*.*",
"lib/**/*.*",
"app/**/*.*",
"packages/**/*.*",
];
const seen = new Set();
for (const pattern of patterns) {
for (const file of fs.glob(pattern)) {
seen.add(file);
}
}
return seen.size;
}
/** Collect named tool/platform signals that feed richer setup prompts. */
function collectNamedSignals(fs, detectors) {
return detectors
.filter((detector) => hasAnyPath(fs, detector.paths) || hasAnyGlob(fs, detector.globs))
.map((detector) => detector.tool);
}
/** Search a list of files for a regex pattern without crashing on missing files. */
function fileContainsPattern(fs, paths, pattern) {
return paths.some((path) => {
const content = fs.readFile(path);
return content !== null && pattern.test(content);
});
}
/** Detect llm integration. */
function detectLLMIntegration(fs) {
return (fileContainsPattern(fs, PROJECT_STACK_LLM_ENV_FILES, /MODEL_PROVIDER|OPENAI_API_KEY|ANTHROPIC_API_KEY|BEDROCK|OLLAMA/i) ||
fileContainsPattern(fs, PROJECT_STACK_LLM_DEPENDENCY_FILES, /anthropic|openai|langchain|llamaindex|strands/i));
}
/** Detect static-analysis tooling from project files. */
// eslint-disable-next-line complexity -- intentional: detection covers many tool/config combos; extracting would fragment the detector.
function detectStaticAnalysis(fs) {
const staticAnalysis = [];
// PHP: PHPStan
const phpstanConfig = fs.readFile("phpstan.neon") ?? fs.readFile("phpstan.neon.dist");
if (phpstanConfig) {
const levelMatch = phpstanConfig.match(/level:\s*(\d+|max)/);
staticAnalysis.push({ tool: "phpstan", level: levelMatch?.[1] ?? null });
}
// Python: mypy
const mypyConfig = fs.readFile("mypy.ini") ?? fs.readFile("setup.cfg");
if (mypyConfig && /\[mypy\]/i.test(mypyConfig)) {
const strictMatch = mypyConfig.match(/strict\s*=\s*(true|false)/i);
staticAnalysis.push({
tool: "mypy",
level: strictMatch?.[1] === "true" ? "strict" : null,
});
}
// Python: ruff
if (fs.exists("ruff.toml") ||
fs.exists(".ruff.toml") ||
fs.readFile("pyproject.toml")?.includes("[tool.ruff")) {
staticAnalysis.push({ tool: "ruff", level: null });
}
// JS/TS: eslint (config files or package.json devDependencies)
const hasEslintConfig = fs.exists("eslint.config.js") ||
fs.exists("eslint.config.mjs") ||
fs.exists("eslint.config.cjs") ||
fs.exists("eslint.config.ts") ||
fs.exists(".eslintrc.json") ||
fs.exists(".eslintrc.js") ||
fs.exists(".eslintrc.yml") ||
fs.exists(".eslintrc");
if (!hasEslintConfig) {
const pkg = fs.readJson("package.json");
const devDeps = (pkg?.devDependencies ?? {});
if (devDeps["eslint"]) {
staticAnalysis.push({ tool: "eslint", level: null });
}
}
else {
staticAnalysis.push({ tool: "eslint", level: null });
}
// JS/TS: biome
if (fs.exists("biome.json") || fs.exists("biome.jsonc")) {
staticAnalysis.push({ tool: "biome", level: null });
}
// Go: golangci-lint
if (fs.exists(".golangci.yml") ||
fs.exists(".golangci.yaml") ||
fs.exists(".golangci.toml")) {
staticAnalysis.push({ tool: "golangci-lint", level: null });
}
// Rust: clippy (detected via Cargo.toml presence - clippy ships with rustup)
if (fs.exists("Cargo.toml")) {
staticAnalysis.push({ tool: "clippy", level: null });
}
// Ruby: rubocop
if (fs.exists(".rubocop.yml") || fs.exists(".rubocop.yaml")) {
staticAnalysis.push({ tool: "rubocop", level: null });
}
// Python: pylint
if (fs.exists(".pylintrc") || fs.exists("pylintrc")) {
staticAnalysis.push({ tool: "pylint", level: null });
}
return staticAnalysis;
}
/** Compliance-sensitive docs are signal-only; audit policy decides whether they matter. */
function detectComplianceSignals(fs) {
return fileContainsPattern(fs, PROJECT_STACK_COMPLIANCE_DOCS, /\bPHI\b|HIPAA|GDPR|patient.*data|health.*record/i);
}
/** Combine formatter-related commands into one searchable string. */
function getFormatterSources(formatCommand) {
return (formatCommand ?? "").toLowerCase();
}
/** Decide whether formatter-gap checks should apply to the given language. */
function shouldCheckFormatter(lang, languages) {
if (lang !== "bash")
return true;
return (languages[0] === "bash" ||
(languages.includes("bash") && languages.length <= 2));
}
/** Detect formatter gaps. */
function detectFormatterGaps(languages, formatCommand) {
const formatterSources = getFormatterSources(formatCommand);
const formatterGaps = [];
for (const lang of languages) {
if (!shouldCheckFormatter(lang, languages))
continue;
const known = PROJECT_STACK_FORMATTER_MAP[lang];
if (!known)
continue;
if (!known.some((formatter) => formatterSources.includes(formatter))) {
formatterGaps.push(lang);
}
}
return formatterGaps;
}
/**
* Aggregate every secondary signal into one ProjectSignals record for the setup
* and audit pipelines. The single entry point so callers run detection once and
* read a complete picture rather than invoking each detector piecemeal.
*
* @param fs - read-only filesystem adapter for the target project
* @param languages - detected languages in precedence order; gates per-language formatter checks
* @param formatCommand - the project's configured format command, or null when none is detected
* @returns the populated signal record; list fields are empty (not null) when nothing is detected
*/
export function detectProjectSignals(fs, languages, formatCommand) {
return {
codeGenTools: collectNamedSignals(fs, PROJECT_STACK_CODE_GENERATION_SIGNALS),
deployPlatforms: collectNamedSignals(fs, PROJECT_STACK_DEPLOYMENT_SIGNALS),
llmIntegration: detectLLMIntegration(fs),
staticAnalysis: detectStaticAnalysis(fs),
complianceSignals: detectComplianceSignals(fs),
formatterGaps: detectFormatterGaps(languages, formatCommand),
};
}
//# sourceMappingURL=project-stack-signals.js.map