forgecraft-mcp 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/registry/remote-gates.d.ts +16 -0
- package/dist/registry/remote-gates.d.ts.map +1 -1
- package/dist/registry/remote-gates.js +56 -0
- package/dist/registry/remote-gates.js.map +1 -1
- package/dist/registry/sentinel-domain-map.d.ts.map +1 -1
- package/dist/registry/sentinel-domain-map.js +16 -9
- package/dist/registry/sentinel-domain-map.js.map +1 -1
- package/dist/registry/sentinel-renderer.d.ts +13 -8
- package/dist/registry/sentinel-renderer.d.ts.map +1 -1
- package/dist/registry/sentinel-renderer.js +440 -162
- package/dist/registry/sentinel-renderer.js.map +1 -1
- package/dist/shared/harness-budget.d.ts +49 -0
- package/dist/shared/harness-budget.d.ts.map +1 -0
- package/dist/shared/harness-budget.js +123 -0
- package/dist/shared/harness-budget.js.map +1 -0
- package/dist/shared/hook-installer.d.ts.map +1 -1
- package/dist/shared/hook-installer.js +2 -1
- package/dist/shared/hook-installer.js.map +1 -1
- package/dist/tools/close-cycle-helpers.d.ts +9 -0
- package/dist/tools/close-cycle-helpers.d.ts.map +1 -1
- package/dist/tools/close-cycle-helpers.js.map +1 -1
- package/dist/tools/close-cycle.d.ts.map +1 -1
- package/dist/tools/close-cycle.js +29 -0
- package/dist/tools/close-cycle.js.map +1 -1
- package/dist/tools/contribute-gate.d.ts +30 -4
- package/dist/tools/contribute-gate.d.ts.map +1 -1
- package/dist/tools/contribute-gate.js +180 -66
- package/dist/tools/contribute-gate.js.map +1 -1
- package/dist/tools/gate-genesis.d.ts +47 -0
- package/dist/tools/gate-genesis.d.ts.map +1 -0
- package/dist/tools/gate-genesis.js +241 -0
- package/dist/tools/gate-genesis.js.map +1 -0
- package/dist/tools/learning-graph.d.ts +31 -0
- package/dist/tools/learning-graph.d.ts.map +1 -0
- package/dist/tools/learning-graph.js +266 -0
- package/dist/tools/learning-graph.js.map +1 -0
- package/dist/tools/setup-artifact-writers.d.ts +15 -3
- package/dist/tools/setup-artifact-writers.d.ts.map +1 -1
- package/dist/tools/setup-artifact-writers.js +149 -13
- package/dist/tools/setup-artifact-writers.js.map +1 -1
- package/dist/tools/setup-phase2.d.ts +9 -0
- package/dist/tools/setup-phase2.d.ts.map +1 -1
- package/dist/tools/setup-phase2.js +13 -0
- package/dist/tools/setup-phase2.js.map +1 -1
- package/dist/tools/setup-project.d.ts +6 -0
- package/dist/tools/setup-project.d.ts.map +1 -1
- package/dist/tools/setup-project.js +21 -4
- package/dist/tools/setup-project.js.map +1 -1
- package/package.json +99 -98
- package/templates/api/instructions.yaml +50 -188
- package/templates/universal/instructions.yaml +194 -1003
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Gate contribution to the public quality-gates registry.
|
|
3
|
+
*
|
|
4
|
+
* Submission mechanism: GitHub issues on jghiringhelli/quality-gates
|
|
5
|
+
* (there is no ForgeCraft API server — by design).
|
|
6
|
+
*
|
|
7
|
+
* Primary: `gh issue create` — works when the dev has the GitHub CLI
|
|
8
|
+
* installed and authenticated (the common case).
|
|
9
|
+
* Fallback: a pre-filled GitHub issue URL written to the pending file —
|
|
10
|
+
* one click opens the proposal in the browser, body pre-populated.
|
|
11
|
+
*
|
|
12
|
+
* Issue format matches .github/ISSUE_TEMPLATE/quality-gate-proposal.md in
|
|
13
|
+
* the registry repo (labels: gate-proposal, status:pending-review).
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync, } from "fs";
|
|
2
16
|
import { join } from "path";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
3
19
|
import { getContributableGates } from "../shared/project-gates.js";
|
|
4
20
|
/**
|
|
5
21
|
* Validate that a generalizable gate satisfies all five community convergence attributes.
|
|
@@ -36,21 +52,18 @@ function validateConvergenceAttributes(gate) {
|
|
|
36
52
|
}
|
|
37
53
|
const PENDING_CONTRIBUTIONS_FILE = ".forgecraft/pending-contributions.json";
|
|
38
54
|
const SUBMITTED_CONTRIBUTIONS_FILE = ".forgecraft/contributions.json";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
const DEFAULT_SERVER_URL = "https://api.forgecraft.tools";
|
|
55
|
+
const DEFAULT_REGISTRY_REPO = "jghiringhelli/quality-gates";
|
|
56
|
+
const ISSUE_LABELS = "gate-proposal,status:pending-review";
|
|
43
57
|
function readContributionConfig(projectRoot) {
|
|
44
58
|
const forgecraftPath = join(projectRoot, "forgecraft.yaml");
|
|
45
59
|
if (!existsSync(forgecraftPath)) {
|
|
46
|
-
return { contributeGates: false,
|
|
60
|
+
return { contributeGates: false, registryRepo: DEFAULT_REGISTRY_REPO };
|
|
47
61
|
}
|
|
48
62
|
try {
|
|
49
63
|
// Simple parse — avoid importing js-yaml to keep this lightweight
|
|
50
64
|
const raw = readFileSync(forgecraftPath, "utf-8");
|
|
51
65
|
const contributeMatch = raw.match(/contribute_gates:\s*(\S+)/);
|
|
52
|
-
const
|
|
53
|
-
const apiKeyMatch = raw.match(/api_key:\s*(\S+)/);
|
|
66
|
+
const repoMatch = raw.match(/registry_repo:\s*(\S+)/);
|
|
54
67
|
const githubMatch = raw.match(/github_user:\s*(\S+)/);
|
|
55
68
|
const val = contributeMatch?.[1];
|
|
56
69
|
const contributeGates = val === "anonymous"
|
|
@@ -60,13 +73,12 @@ function readContributionConfig(projectRoot) {
|
|
|
60
73
|
: false;
|
|
61
74
|
return {
|
|
62
75
|
contributeGates,
|
|
63
|
-
|
|
64
|
-
apiKey: apiKeyMatch?.[1],
|
|
76
|
+
registryRepo: repoMatch?.[1] ?? DEFAULT_REGISTRY_REPO,
|
|
65
77
|
githubUser: githubMatch?.[1],
|
|
66
78
|
};
|
|
67
79
|
}
|
|
68
80
|
catch {
|
|
69
|
-
return { contributeGates: false,
|
|
81
|
+
return { contributeGates: false, registryRepo: DEFAULT_REGISTRY_REPO };
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
@@ -101,80 +113,182 @@ function recordSubmission(projectRoot, gate) {
|
|
|
101
113
|
writeFileSync(filePath, JSON.stringify([...existing, gate], null, 2) + "\n", "utf-8");
|
|
102
114
|
}
|
|
103
115
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @param gate - The project gate to submit.
|
|
107
|
-
* @param mode - Contribution mode: anonymous or attributed.
|
|
108
|
-
* @param serverUrl - Target API URL.
|
|
109
|
-
* @param githubUser - GitHub username for attributed mode.
|
|
110
|
-
* @param projectType - Optional project type context.
|
|
111
|
-
* @param experimentId - Optional experiment identifier to tag the submission.
|
|
112
|
-
* @returns Submission result with status and optional issue URL.
|
|
116
|
+
* Build the GitHub issue title for a gate proposal.
|
|
117
|
+
* Matches the registry's issue template: "[Gate Proposal] <gate-id>".
|
|
113
118
|
*/
|
|
114
|
-
|
|
119
|
+
function buildIssueTitle(gate) {
|
|
120
|
+
return `[Gate Proposal] ${gate.id}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the GitHub issue body matching the registry's
|
|
124
|
+
* .github/ISSUE_TEMPLATE/quality-gate-proposal.md format.
|
|
125
|
+
*/
|
|
126
|
+
function buildIssueBody(gate, mode, githubUser, experimentId) {
|
|
127
|
+
const contributor = mode === "attributed" && githubUser ? `@${githubUser}` : "anonymous";
|
|
128
|
+
const tags = Array.isArray(gate.tags) ? gate.tags.join(" | ") : "UNIVERSAL";
|
|
129
|
+
return [
|
|
130
|
+
`## Gate Proposal`,
|
|
131
|
+
``,
|
|
132
|
+
`**Contributor**: ${contributor}`,
|
|
133
|
+
`**Project type**: ${gate.domain ?? "general"}`,
|
|
134
|
+
experimentId ? `**Experiment**: ${experimentId}` : ``,
|
|
135
|
+
``,
|
|
136
|
+
`---`,
|
|
137
|
+
``,
|
|
138
|
+
`### Gate Definition`,
|
|
139
|
+
``,
|
|
140
|
+
`**ID**: \`${gate.id}\``,
|
|
141
|
+
`**Title**: ${gate.title}`,
|
|
142
|
+
`**Category**: ${gate.domain ?? "other"}`,
|
|
143
|
+
`**GS Property**: ${gate.gsProperty ?? ""}`,
|
|
144
|
+
`**Phase**: ${gate.phase ?? "development"}`,
|
|
145
|
+
`**Hook**: ${gate.hook ?? ""}`,
|
|
146
|
+
`**Tags**: ${tags}`,
|
|
147
|
+
``,
|
|
148
|
+
`### Description`,
|
|
149
|
+
gate.description ?? "",
|
|
150
|
+
``,
|
|
151
|
+
`### Check`,
|
|
152
|
+
"```",
|
|
153
|
+
gate.check ?? "",
|
|
154
|
+
"```",
|
|
155
|
+
``,
|
|
156
|
+
`### Pass Criterion`,
|
|
157
|
+
gate.passCriterion ?? "",
|
|
158
|
+
``,
|
|
159
|
+
`### Evidence`,
|
|
160
|
+
`> ${(gate.evidence ?? "").replace(/\n/g, "\n> ")}`,
|
|
161
|
+
``,
|
|
162
|
+
`---`,
|
|
163
|
+
``,
|
|
164
|
+
`*Submitted via \`forgecraft-mcp contribute_gate\`. By submitting this proposal,`,
|
|
165
|
+
`the contributor agrees the gate definition may be published under CC-BY-4.0`,
|
|
166
|
+
`in the quality-gates registry.*`,
|
|
167
|
+
]
|
|
168
|
+
.filter((line) => line !== undefined)
|
|
169
|
+
.join("\n");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a pre-filled GitHub "new issue" URL — the no-auth fallback.
|
|
173
|
+
* One click opens the proposal in the browser with the body pre-populated.
|
|
174
|
+
* GitHub caps URLs around 8 KB; the body is truncated defensively.
|
|
175
|
+
*/
|
|
176
|
+
function buildFallbackIssueUrl(registryRepo, title, body) {
|
|
177
|
+
const truncatedBody = body.length > 5500
|
|
178
|
+
? body.slice(0, 5500) +
|
|
179
|
+
"\n\n<!-- truncated — full gate YAML in .forgecraft/gates/active/ -->"
|
|
180
|
+
: body;
|
|
181
|
+
const params = new URLSearchParams({
|
|
182
|
+
title,
|
|
183
|
+
labels: ISSUE_LABELS,
|
|
184
|
+
body: truncatedBody,
|
|
185
|
+
});
|
|
186
|
+
return `https://github.com/${registryRepo}/issues/new?${params.toString()}`;
|
|
187
|
+
}
|
|
188
|
+
/** Default GhRunner — invokes the real GitHub CLI. Exported for direct testing. */
|
|
189
|
+
export function runGhCli(args) {
|
|
190
|
+
// Safety net: never create real GitHub issues from a test run.
|
|
191
|
+
// Tests exercising the success path inject a mock ghRunner instead.
|
|
192
|
+
if (process.env["VITEST"] || process.env["NODE_ENV"] === "test") {
|
|
193
|
+
return { ok: false, stdout: "" };
|
|
194
|
+
}
|
|
115
195
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
121
|
-
const response = await fetch(`${serverUrl}/contribute/gate`, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers,
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
gate: {
|
|
126
|
-
id: gate.id,
|
|
127
|
-
title: gate.title,
|
|
128
|
-
description: gate.description,
|
|
129
|
-
domain: gate.domain,
|
|
130
|
-
gsProperty: gate.gsProperty,
|
|
131
|
-
phase: gate.phase,
|
|
132
|
-
hook: gate.hook,
|
|
133
|
-
check: gate.check,
|
|
134
|
-
passCriterion: gate.passCriterion,
|
|
135
|
-
tags: gate.tags,
|
|
136
|
-
evidence: gate.evidence,
|
|
137
|
-
convergenceAttributes: gate.convergenceAttributes,
|
|
138
|
-
},
|
|
139
|
-
mode,
|
|
140
|
-
attribution: mode === "attributed"
|
|
141
|
-
? { github: githubUser, projectType }
|
|
142
|
-
: undefined,
|
|
143
|
-
...(experimentId ? { experimentId } : {}),
|
|
144
|
-
}),
|
|
145
|
-
signal: AbortSignal.timeout(8000),
|
|
196
|
+
const result = spawnSync("gh", args, {
|
|
197
|
+
encoding: "utf-8",
|
|
198
|
+
timeout: 15_000,
|
|
199
|
+
windowsHide: true,
|
|
146
200
|
});
|
|
147
|
-
if (
|
|
148
|
-
return {
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
201
|
+
if (result.error || result.status !== 0) {
|
|
202
|
+
return { ok: false, stdout: result.stdout ?? "" };
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, stdout: (result.stdout ?? "").trim() };
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { ok: false, stdout: "" };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Submit a gate proposal as a GitHub issue on the registry repo.
|
|
212
|
+
*
|
|
213
|
+
* Tries `gh issue create` first (authenticated CLI). On any failure, returns
|
|
214
|
+
* status "pending" with a pre-filled issue URL the dev can open manually.
|
|
215
|
+
*
|
|
216
|
+
* @param gate - The project gate to submit
|
|
217
|
+
* @param mode - Contribution mode: anonymous or attributed
|
|
218
|
+
* @param registryRepo - Target repo ("owner/name")
|
|
219
|
+
* @param githubUser - GitHub username for attributed mode
|
|
220
|
+
* @param experimentId - Optional experiment identifier
|
|
221
|
+
* @param ghRunner - Injectable gh CLI runner (tests)
|
|
222
|
+
* @returns Submission result with status and issue URL
|
|
223
|
+
*/
|
|
224
|
+
function submitGateAsIssue(gate, mode, registryRepo, githubUser, experimentId, ghRunner = runGhCli) {
|
|
225
|
+
const title = buildIssueTitle(gate);
|
|
226
|
+
const body = buildIssueBody(gate, mode, githubUser, experimentId);
|
|
227
|
+
// Primary: gh CLI. --body-file avoids all shell-escaping issues.
|
|
228
|
+
let bodyDir;
|
|
229
|
+
try {
|
|
230
|
+
bodyDir = mkdtempSync(join(tmpdir(), "fc-gate-"));
|
|
231
|
+
const bodyFile = join(bodyDir, "issue-body.md");
|
|
232
|
+
writeFileSync(bodyFile, body, "utf-8");
|
|
233
|
+
const result = ghRunner([
|
|
234
|
+
"issue",
|
|
235
|
+
"create",
|
|
236
|
+
"--repo",
|
|
237
|
+
registryRepo,
|
|
238
|
+
"--title",
|
|
239
|
+
title,
|
|
240
|
+
"--body-file",
|
|
241
|
+
bodyFile,
|
|
242
|
+
"--label",
|
|
243
|
+
ISSUE_LABELS,
|
|
244
|
+
]);
|
|
245
|
+
if (result.ok) {
|
|
246
|
+
// gh prints the created issue URL as the last stdout line
|
|
247
|
+
const url = result.stdout
|
|
248
|
+
.split("\n")
|
|
249
|
+
.reverse()
|
|
250
|
+
.find((l) => l.startsWith("https://"));
|
|
251
|
+
return { status: "submitted", issueUrl: url };
|
|
252
|
+
}
|
|
154
253
|
}
|
|
155
254
|
catch {
|
|
156
|
-
|
|
255
|
+
// fall through to URL fallback
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (bodyDir) {
|
|
259
|
+
try {
|
|
260
|
+
rmSync(bodyDir, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
/* temp cleanup is best-effort */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
157
266
|
}
|
|
267
|
+
// Fallback: pre-filled issue URL — dev opens it in the browser.
|
|
268
|
+
return {
|
|
269
|
+
status: "pending",
|
|
270
|
+
issueUrl: buildFallbackIssueUrl(registryRepo, title, body),
|
|
271
|
+
};
|
|
158
272
|
}
|
|
159
273
|
/**
|
|
160
274
|
* Contributes all generalizable gates from .forgecraft/project-gates.yaml.
|
|
161
275
|
* - Reads contribute_gates setting from forgecraft.yaml
|
|
162
276
|
* - Skips gates already submitted (tracked in .forgecraft/contributions.json)
|
|
163
|
-
* -
|
|
164
|
-
*
|
|
277
|
+
* - Creates a GitHub issue on the registry repo via gh CLI when available,
|
|
278
|
+
* otherwise queues a pre-filled issue URL for one-click manual submission
|
|
279
|
+
* - Never throws — all failures are recorded as skipped or pending
|
|
165
280
|
*
|
|
166
281
|
* @param options - Contribution options including project root and optional overrides.
|
|
167
282
|
* @returns Result containing submitted, skipped gates and optional pending file path.
|
|
168
283
|
*/
|
|
169
284
|
export async function contributeGates(options) {
|
|
170
|
-
const { projectRoot, dryRun = false, experimentId } = options;
|
|
285
|
+
const { projectRoot, dryRun = false, experimentId, ghRunner } = options;
|
|
171
286
|
const config = readContributionConfig(projectRoot);
|
|
172
287
|
if (!config.contributeGates) {
|
|
173
288
|
return { submitted: [], skipped: [], pendingFile: undefined };
|
|
174
289
|
}
|
|
175
290
|
const mode = config.contributeGates;
|
|
176
|
-
const
|
|
177
|
-
const apiKey = options.apiKey ?? config.apiKey;
|
|
291
|
+
const registryRepo = options.registryRepo ?? config.registryRepo;
|
|
178
292
|
const gates = getContributableGates(projectRoot);
|
|
179
293
|
const alreadySubmitted = getAlreadySubmitted(projectRoot);
|
|
180
294
|
const submitted = [];
|
|
@@ -200,12 +314,12 @@ export async function contributeGates(options) {
|
|
|
200
314
|
submitted.push({ gateId: gate.id, mode, status: "pending" });
|
|
201
315
|
continue;
|
|
202
316
|
}
|
|
203
|
-
const result =
|
|
317
|
+
const result = submitGateAsIssue(gate, mode, registryRepo, config.githubUser, experimentId, ghRunner);
|
|
204
318
|
const contributed = { gateId: gate.id, ...result, mode };
|
|
205
319
|
submitted.push(contributed);
|
|
206
320
|
recordSubmission(projectRoot, contributed);
|
|
207
321
|
}
|
|
208
|
-
// Write pending contributions
|
|
322
|
+
// Write pending contributions (with their one-click issue URLs) for manual submission
|
|
209
323
|
const pending = submitted.filter((s) => s.status === "pending");
|
|
210
324
|
let pendingFile;
|
|
211
325
|
if (pending.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contribute-gate.js","sourceRoot":"","sources":["../../src/tools/contribute-gate.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"contribute-gate.js","sourceRoot":"","sources":["../../src/tools/contribute-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate genesis — propose new quality gates from observed development friction.
|
|
3
|
+
*
|
|
4
|
+
* Two signal sources:
|
|
5
|
+
* 1. `.forgecraft/gate-violations.jsonl` — the same hook violated repeatedly
|
|
6
|
+
* means the team keeps hitting a failure mode worth formalizing.
|
|
7
|
+
* 2. `.claude/corrections.md` — repeated AI corrections in the same category
|
|
8
|
+
* mean an unenforced convention; a gate makes it structural.
|
|
9
|
+
*
|
|
10
|
+
* Candidates become DRAFT YAML stubs in `.forgecraft/gates/drafts/` — never
|
|
11
|
+
* auto-activated (human judgment). When the dev fills in `evidence` and sets
|
|
12
|
+
* `generalizable: true`, the existing contribute flow takes the gate to the
|
|
13
|
+
* public registry as a GitHub issue. This completes the community flywheel:
|
|
14
|
+
* violations → drafts → active gates → registry → installed in other projects.
|
|
15
|
+
*/
|
|
16
|
+
export interface GateCandidate {
|
|
17
|
+
/** Proposed gate id, e.g. "auto-gate-hardcoded-url". */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly source: "violations" | "corrections";
|
|
20
|
+
/** The hook name or correction category that triggered the proposal. */
|
|
21
|
+
readonly pattern: string;
|
|
22
|
+
readonly occurrences: number;
|
|
23
|
+
/** Up to MAX_EXAMPLES sample messages for the draft's context. */
|
|
24
|
+
readonly examples: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Scan violation log + corrections log and return gate candidates for
|
|
28
|
+
* patterns that repeat above threshold and aren't already covered by an
|
|
29
|
+
* active or draft gate.
|
|
30
|
+
*
|
|
31
|
+
* Never throws — missing or malformed files yield an empty list.
|
|
32
|
+
*
|
|
33
|
+
* @param projectRoot - Project root directory
|
|
34
|
+
* @returns Candidates sorted by occurrence count, highest first
|
|
35
|
+
*/
|
|
36
|
+
export declare function proposeGateCandidates(projectRoot: string): GateCandidate[];
|
|
37
|
+
/**
|
|
38
|
+
* Write draft gate YAML stubs for the given candidates.
|
|
39
|
+
* Drafts land in `.forgecraft/gates/drafts/<id>.yaml` — idempotent, never
|
|
40
|
+
* overwrites, never auto-activates.
|
|
41
|
+
*
|
|
42
|
+
* @param projectRoot - Project root directory
|
|
43
|
+
* @param candidates - Candidates from proposeGateCandidates
|
|
44
|
+
* @returns Relative paths of draft files written
|
|
45
|
+
*/
|
|
46
|
+
export declare function writeGateDrafts(projectRoot: string, candidates: readonly GateCandidate[]): string[];
|
|
47
|
+
//# sourceMappingURL=gate-genesis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-genesis.d.ts","sourceRoot":"","sources":["../../src/tools/gate-genesis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AASD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CAO1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,SAAS,aAAa,EAAE,GACnC,MAAM,EAAE,CAoBV"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate genesis — propose new quality gates from observed development friction.
|
|
3
|
+
*
|
|
4
|
+
* Two signal sources:
|
|
5
|
+
* 1. `.forgecraft/gate-violations.jsonl` — the same hook violated repeatedly
|
|
6
|
+
* means the team keeps hitting a failure mode worth formalizing.
|
|
7
|
+
* 2. `.claude/corrections.md` — repeated AI corrections in the same category
|
|
8
|
+
* mean an unenforced convention; a gate makes it structural.
|
|
9
|
+
*
|
|
10
|
+
* Candidates become DRAFT YAML stubs in `.forgecraft/gates/drafts/` — never
|
|
11
|
+
* auto-activated (human judgment). When the dev fills in `evidence` and sets
|
|
12
|
+
* `generalizable: true`, the existing contribute flow takes the gate to the
|
|
13
|
+
* public registry as a GitHub issue. This completes the community flywheel:
|
|
14
|
+
* violations → drafts → active gates → registry → installed in other projects.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { dump as yamlDump } from "js-yaml";
|
|
19
|
+
/** Minimum repeats before a violation pattern becomes a candidate. */
|
|
20
|
+
const VIOLATION_THRESHOLD = 3;
|
|
21
|
+
/** Minimum repeats before a correction category becomes a candidate. */
|
|
22
|
+
const CORRECTION_THRESHOLD = 2;
|
|
23
|
+
/** Max sample messages carried into the draft for context. */
|
|
24
|
+
const MAX_EXAMPLES = 3;
|
|
25
|
+
/**
|
|
26
|
+
* Scan violation log + corrections log and return gate candidates for
|
|
27
|
+
* patterns that repeat above threshold and aren't already covered by an
|
|
28
|
+
* active or draft gate.
|
|
29
|
+
*
|
|
30
|
+
* Never throws — missing or malformed files yield an empty list.
|
|
31
|
+
*
|
|
32
|
+
* @param projectRoot - Project root directory
|
|
33
|
+
* @returns Candidates sorted by occurrence count, highest first
|
|
34
|
+
*/
|
|
35
|
+
export function proposeGateCandidates(projectRoot) {
|
|
36
|
+
const covered = collectCoveredPatterns(projectRoot);
|
|
37
|
+
const candidates = [
|
|
38
|
+
...candidatesFromViolations(projectRoot, covered),
|
|
39
|
+
...candidatesFromCorrections(projectRoot, covered),
|
|
40
|
+
];
|
|
41
|
+
return candidates.sort((a, b) => b.occurrences - a.occurrences);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write draft gate YAML stubs for the given candidates.
|
|
45
|
+
* Drafts land in `.forgecraft/gates/drafts/<id>.yaml` — idempotent, never
|
|
46
|
+
* overwrites, never auto-activates.
|
|
47
|
+
*
|
|
48
|
+
* @param projectRoot - Project root directory
|
|
49
|
+
* @param candidates - Candidates from proposeGateCandidates
|
|
50
|
+
* @returns Relative paths of draft files written
|
|
51
|
+
*/
|
|
52
|
+
export function writeGateDrafts(projectRoot, candidates) {
|
|
53
|
+
const written = [];
|
|
54
|
+
if (candidates.length === 0)
|
|
55
|
+
return written;
|
|
56
|
+
const draftsDir = join(projectRoot, ".forgecraft", "gates", "drafts");
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
const filePath = join(draftsDir, `${candidate.id}.yaml`);
|
|
59
|
+
if (existsSync(filePath))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(draftsDir, { recursive: true });
|
|
63
|
+
writeFileSync(filePath, buildDraftYaml(candidate), "utf-8");
|
|
64
|
+
written.push(`.forgecraft/gates/drafts/${candidate.id}.yaml`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Single draft failure is non-fatal
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return written;
|
|
71
|
+
}
|
|
72
|
+
// ── Signal extraction ─────────────────────────────────────────────────
|
|
73
|
+
function candidatesFromViolations(projectRoot, covered) {
|
|
74
|
+
const filePath = join(projectRoot, ".forgecraft", "gate-violations.jsonl");
|
|
75
|
+
if (!existsSync(filePath))
|
|
76
|
+
return [];
|
|
77
|
+
const byHook = new Map();
|
|
78
|
+
try {
|
|
79
|
+
for (const line of readFileSync(filePath, "utf-8").split("\n")) {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (!trimmed)
|
|
82
|
+
continue;
|
|
83
|
+
let entry;
|
|
84
|
+
try {
|
|
85
|
+
entry = JSON.parse(trimmed);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue; // skip malformed lines
|
|
89
|
+
}
|
|
90
|
+
if (!entry.hook)
|
|
91
|
+
continue;
|
|
92
|
+
const messages = byHook.get(entry.hook) ?? [];
|
|
93
|
+
messages.push(entry.message ?? "");
|
|
94
|
+
byHook.set(entry.hook, messages);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const candidates = [];
|
|
101
|
+
for (const [hook, messages] of byHook) {
|
|
102
|
+
if (messages.length < VIOLATION_THRESHOLD)
|
|
103
|
+
continue;
|
|
104
|
+
const pattern = normalizePattern(hook);
|
|
105
|
+
if (covered.has(pattern))
|
|
106
|
+
continue;
|
|
107
|
+
candidates.push({
|
|
108
|
+
id: `auto-gate-${pattern}`,
|
|
109
|
+
source: "violations",
|
|
110
|
+
pattern: hook,
|
|
111
|
+
occurrences: messages.length,
|
|
112
|
+
examples: dedupe(messages).slice(0, MAX_EXAMPLES),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return candidates;
|
|
116
|
+
}
|
|
117
|
+
function candidatesFromCorrections(projectRoot, covered) {
|
|
118
|
+
const filePath = join(projectRoot, ".claude", "corrections.md");
|
|
119
|
+
if (!existsSync(filePath))
|
|
120
|
+
return [];
|
|
121
|
+
// Entry format: YYYY-MM-DD | [category] description
|
|
122
|
+
const entryPattern = /^\d{4}-\d{2}-\d{2}\s*\|\s*\[([^\]]+)\]\s*(.+)$/;
|
|
123
|
+
const byCategory = new Map();
|
|
124
|
+
try {
|
|
125
|
+
let inComment = false;
|
|
126
|
+
for (const line of readFileSync(filePath, "utf-8").split("\n")) {
|
|
127
|
+
// Skip the commented-out examples in the stub
|
|
128
|
+
if (line.includes("<!--"))
|
|
129
|
+
inComment = true;
|
|
130
|
+
if (inComment) {
|
|
131
|
+
if (line.includes("-->"))
|
|
132
|
+
inComment = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const match = entryPattern.exec(line.trim());
|
|
136
|
+
if (!match)
|
|
137
|
+
continue;
|
|
138
|
+
const category = match[1].trim().toLowerCase();
|
|
139
|
+
const entries = byCategory.get(category) ?? [];
|
|
140
|
+
entries.push(match[2].trim());
|
|
141
|
+
byCategory.set(category, entries);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const candidates = [];
|
|
148
|
+
for (const [category, entries] of byCategory) {
|
|
149
|
+
if (entries.length < CORRECTION_THRESHOLD)
|
|
150
|
+
continue;
|
|
151
|
+
const pattern = normalizePattern(category);
|
|
152
|
+
if (covered.has(pattern))
|
|
153
|
+
continue;
|
|
154
|
+
candidates.push({
|
|
155
|
+
id: `auto-gate-${pattern}`,
|
|
156
|
+
source: "corrections",
|
|
157
|
+
pattern: category,
|
|
158
|
+
occurrences: entries.length,
|
|
159
|
+
examples: dedupe(entries).slice(0, MAX_EXAMPLES),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return candidates;
|
|
163
|
+
}
|
|
164
|
+
// ── Coverage check ────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Patterns already covered by an active or draft gate — don't re-propose.
|
|
167
|
+
* A gate covers a pattern when its id or hook field contains the normalized
|
|
168
|
+
* pattern string.
|
|
169
|
+
*/
|
|
170
|
+
function collectCoveredPatterns(projectRoot) {
|
|
171
|
+
const covered = new Set();
|
|
172
|
+
for (const subdir of ["active", "drafts"]) {
|
|
173
|
+
const dir = join(projectRoot, ".forgecraft", "gates", subdir);
|
|
174
|
+
if (!existsSync(dir))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
for (const file of readdirSync(dir)) {
|
|
178
|
+
if (!file.endsWith(".yaml") && !file.endsWith(".yml"))
|
|
179
|
+
continue;
|
|
180
|
+
// Gate id from filename: auto-gate-<pattern>.yaml or <pattern>.yaml
|
|
181
|
+
const base = file.replace(/\.(yaml|yml)$/, "");
|
|
182
|
+
covered.add(normalizePattern(base.replace(/^auto-gate-/, "")));
|
|
183
|
+
covered.add(normalizePattern(base));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Unreadable dir — treat as no coverage
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return covered;
|
|
191
|
+
}
|
|
192
|
+
// ── Draft rendering ───────────────────────────────────────────────────
|
|
193
|
+
function buildDraftYaml(candidate) {
|
|
194
|
+
const header = [
|
|
195
|
+
`# DRAFT gate — generated by gate genesis from repeated ${candidate.source}.`,
|
|
196
|
+
`# Pattern "${candidate.pattern}" occurred ${candidate.occurrences} times.`,
|
|
197
|
+
`#`,
|
|
198
|
+
`# To activate: fill in the FILL fields, move this file to`,
|
|
199
|
+
`# .forgecraft/gates/active/, and set generalizable: true if the gate`,
|
|
200
|
+
`# would help other projects (close_cycle will then propose it to the`,
|
|
201
|
+
`# community registry as a GitHub issue).`,
|
|
202
|
+
``,
|
|
203
|
+
].join("\n");
|
|
204
|
+
const body = yamlDump({
|
|
205
|
+
id: candidate.id,
|
|
206
|
+
title: `<FILL: human-readable title for the ${candidate.pattern} gate>`,
|
|
207
|
+
description: `Formalizes a repeated ${candidate.source === "violations" ? "hook violation" : "AI correction"}: ${candidate.pattern}`,
|
|
208
|
+
domain: "<FILL: security | test-quality | api-contract | environment-hygiene | other>",
|
|
209
|
+
gsProperty: "defended",
|
|
210
|
+
phase: "development",
|
|
211
|
+
hook: candidate.source === "violations" ? candidate.pattern : "pre-commit",
|
|
212
|
+
check: "<FILL: executable step-by-step check — no interpretation required>",
|
|
213
|
+
passCriterion: "<FILL: binary pass statement>",
|
|
214
|
+
implementation: "logic",
|
|
215
|
+
source: "project",
|
|
216
|
+
status: "draft",
|
|
217
|
+
// Provenance: "genesis" = the system detected the need from repeated
|
|
218
|
+
// friction. AI/dev-created gates use "organic" (see Gate Awareness in
|
|
219
|
+
// .claude/lifecycle.md). Tracked so the registry can distinguish gates
|
|
220
|
+
// born from observed failure vs. proactive judgment.
|
|
221
|
+
origin: "genesis",
|
|
222
|
+
detectedFrom: candidate.source,
|
|
223
|
+
generalizable: false,
|
|
224
|
+
evidence: `Observed ${candidate.occurrences}x in this project. Examples: ${candidate.examples.join(" | ")}`,
|
|
225
|
+
observedExamples: [...candidate.examples],
|
|
226
|
+
}, { lineWidth: 100, noRefs: true });
|
|
227
|
+
return header + body;
|
|
228
|
+
}
|
|
229
|
+
// ── Utilities ─────────────────────────────────────────────────────────
|
|
230
|
+
function normalizePattern(raw) {
|
|
231
|
+
return raw
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/^pre-commit-|^pre-push-|^post-commit-/, "")
|
|
234
|
+
.replace(/\.sh$/, "")
|
|
235
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
236
|
+
.replace(/^-+|-+$/g, "");
|
|
237
|
+
}
|
|
238
|
+
function dedupe(items) {
|
|
239
|
+
return [...new Set(items.filter((m) => m.trim()))];
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=gate-genesis.js.map
|