fix(review): address review findings (guardrails, prompts, tests, docs)
- permission-gate: block refs/tags/v* pushes, rm -r -f separated flags, git reset --hard, git clean (verified against actual bypasses) - prompts: /bugfix /feature /review no longer hardcode go-standards — reference profile-gated <lang>-standards instead - company-context: drop hardcoded Go stack, note TRACKER_URL priority, warn on context truncation instead of silently dropping rules - repo-map/docs-map: graceful degradation when config values are TODO - /kit-doctor: warn on unfilled config fields (repoMap/trackerUrl/docsUrl) - audit: retry POSTs to endpoint (3 attempts, backoff), still best-effort - install.sh: remove TODO course URL from cheat sheet - tests: expand guardrails (43 node checks), add shell tests for create-mr.sh (scp/https origin parse, GITLAB_HOST override, protected branch refusal), cover company-context lib (normalize/fetch/truncation) and mcp-bridge - commit package-lock.json for reproducible installs - document npm test Node >= 22.6 requirement (type stripping)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Shared pure helpers for the company-context extension (config loading,
|
||||
* normalization, remote fetch, template rendering). Kept free of the pi API
|
||||
* so it can be unit-tested directly (see test/guardrails.test.ts).
|
||||
*
|
||||
* CONFIGURE: values live in config/company.json; wording in company-context.md.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
|
||||
export interface CompanyConfig {
|
||||
gitHost: string;
|
||||
trackerUrl: string;
|
||||
docsUrl: string;
|
||||
repoMap: string;
|
||||
rules: string[];
|
||||
remoteConfigUrl: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: CompanyConfig = {
|
||||
gitHost: "TODO",
|
||||
trackerUrl: "TODO",
|
||||
docsUrl: "TODO",
|
||||
repoMap: "TODO",
|
||||
rules: [],
|
||||
remoteConfigUrl: null,
|
||||
};
|
||||
|
||||
export const MAX_CONTEXT_LINES = 40;
|
||||
|
||||
/** Keep only known keys and coerce types, so junk in remote/local JSON is ignored. */
|
||||
export function normalize(raw: Record<string, unknown>): CompanyConfig {
|
||||
const str = (v: unknown, d: string) => (typeof v === "string" ? v : d);
|
||||
return {
|
||||
gitHost: str(raw.gitHost, DEFAULT_CONFIG.gitHost),
|
||||
trackerUrl: str(raw.trackerUrl, DEFAULT_CONFIG.trackerUrl),
|
||||
docsUrl: str(raw.docsUrl, DEFAULT_CONFIG.docsUrl),
|
||||
repoMap: str(raw.repoMap, DEFAULT_CONFIG.repoMap),
|
||||
rules: Array.isArray(raw.rules) ? raw.rules.filter((r): r is string => typeof r === "string") : [],
|
||||
remoteConfigUrl: typeof raw.remoteConfigUrl === "string" ? raw.remoteConfigUrl : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read and validate a local config file, falling back to defaults on any error. */
|
||||
export function readLocalConfig(configPath: string): CompanyConfig {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
return normalize(raw);
|
||||
} catch {
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to fetch a fresh config from a URL. Returns null on any failure.
|
||||
* `fetchImpl` is injectable for tests (defaults to the global fetch).
|
||||
*/
|
||||
export async function fetchRemoteConfig(
|
||||
url: string,
|
||||
fetchImpl: typeof fetch = fetch,
|
||||
timeoutMs = 2500,
|
||||
): Promise<CompanyConfig | null> {
|
||||
try {
|
||||
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
if (!res.ok) return null;
|
||||
const raw = (await res.json()) as Record<string, unknown>;
|
||||
return normalize(raw);
|
||||
} catch {
|
||||
return null; // network error, timeout, bad JSON — fall back silently
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the context template; fall back to a minimal inline template. */
|
||||
export function loadTemplate(templatePath: string): string {
|
||||
try {
|
||||
return fs.readFileSync(templatePath, "utf8");
|
||||
} catch {
|
||||
return [
|
||||
"# Корпоративный контекст",
|
||||
"git-хост: {{gitHost}}",
|
||||
"трекер: {{trackerUrl}}",
|
||||
"документация: {{docsUrl}}",
|
||||
"репозитории: {{repoMap}}",
|
||||
"правила:",
|
||||
"{{rules}}",
|
||||
].join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
export interface RenderedContext {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the corporate context from the template + effective config.
|
||||
* Reports whether the output was capped at MAX_CONTEXT_LINES so callers can
|
||||
* warn instead of silently dropping rules.
|
||||
*/
|
||||
export function renderContext(template: string, cfg: CompanyConfig): RenderedContext {
|
||||
const rulesBlock = cfg.rules.length > 0 ? cfg.rules.map((r) => `- ${r}`).join("\n") : "- (правила не заданы)";
|
||||
const rendered = template
|
||||
.replaceAll("{{gitHost}}", cfg.gitHost)
|
||||
.replaceAll("{{trackerUrl}}", cfg.trackerUrl)
|
||||
.replaceAll("{{docsUrl}}", cfg.docsUrl)
|
||||
.replaceAll("{{repoMap}}", cfg.repoMap)
|
||||
.replaceAll("{{rules}}", rulesBlock);
|
||||
const lines = rendered.split("\n");
|
||||
const truncated = lines.length > MAX_CONTEXT_LINES;
|
||||
const text = lines.slice(0, MAX_CONTEXT_LINES).join("\n").trimEnd();
|
||||
return { text, truncated };
|
||||
}
|
||||
Reference in New Issue
Block a user