quick-gate 0.2.0-alpha.1
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/LICENSE +190 -0
- package/README.md +153 -0
- package/package.json +45 -0
- package/schemas/agent-brief.schema.json +112 -0
- package/schemas/failures.schema.json +193 -0
- package/src/cli.js +94 -0
- package/src/config.js +38 -0
- package/src/constants.js +20 -0
- package/src/deterministic-prefix.js +66 -0
- package/src/env-check.js +41 -0
- package/src/exec.js +24 -0
- package/src/fs-utils.js +48 -0
- package/src/gates.js +191 -0
- package/src/model-adapter.js +397 -0
- package/src/repair-command.js +290 -0
- package/src/run-command.js +78 -0
- package/src/schema.js +34 -0
- package/src/summarize-command.js +118 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { runCommand as runShell } from './exec.js';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { nowIso, writeJsonFileSync, ensureDirSync } from './fs-utils.js';
|
|
6
|
+
import { runDeterministicGates } from './gates.js';
|
|
7
|
+
import {
|
|
8
|
+
FAILURES_FILE,
|
|
9
|
+
QUICK_GATE_DIR,
|
|
10
|
+
RUN_METADATA_FILE,
|
|
11
|
+
} from './constants.js';
|
|
12
|
+
import { validateAgainstSchema } from './schema.js';
|
|
13
|
+
import { hasGit } from './env-check.js';
|
|
14
|
+
|
|
15
|
+
function gitInfo(cwd) {
|
|
16
|
+
if (!hasGit()) return { repo: undefined, branch: undefined };
|
|
17
|
+
const repoResult = runShell('git config --get remote.origin.url', { cwd });
|
|
18
|
+
const branchResult = runShell('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
19
|
+
return {
|
|
20
|
+
repo: repoResult.exit_code === 0 ? repoResult.stdout.trim() : undefined,
|
|
21
|
+
branch: branchResult.exit_code === 0 ? branchResult.stdout.trim() : undefined,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function executeRun({ mode, changedFiles, cwd = process.cwd() }) {
|
|
26
|
+
ensureDirSync(path.join(cwd, QUICK_GATE_DIR));
|
|
27
|
+
|
|
28
|
+
const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}_${randomUUID().slice(0, 8)}`;
|
|
29
|
+
const startedAt = Date.now();
|
|
30
|
+
const config = loadConfig(cwd);
|
|
31
|
+
|
|
32
|
+
const gateResult = runDeterministicGates({ mode, cwd, config, changedFiles });
|
|
33
|
+
const status = gateResult.findings.length > 0 ? 'fail' : 'pass';
|
|
34
|
+
const git = gitInfo(cwd);
|
|
35
|
+
|
|
36
|
+
const failuresPayload = {
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
run_id: runId,
|
|
39
|
+
mode,
|
|
40
|
+
status,
|
|
41
|
+
timestamp: nowIso(),
|
|
42
|
+
repo: git.repo,
|
|
43
|
+
branch: git.branch,
|
|
44
|
+
changed_files: changedFiles,
|
|
45
|
+
gates: gateResult.gates,
|
|
46
|
+
findings: gateResult.findings,
|
|
47
|
+
inferred_hints: gateResult.findings.map((finding) => ({
|
|
48
|
+
finding_id: finding.id,
|
|
49
|
+
hint: `Start with the deterministic gate failure in ${finding.gate} and inspect command output in run-metadata traces.`,
|
|
50
|
+
confidence: 'low',
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const validation = validateAgainstSchema('failures.schema.json', failuresPayload);
|
|
55
|
+
if (!validation.valid) {
|
|
56
|
+
throw new Error(`failures.json schema validation failed: ${JSON.stringify(validation.errors, null, 2)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const metadataPayload = {
|
|
60
|
+
run_id: runId,
|
|
61
|
+
mode,
|
|
62
|
+
started_at: new Date(startedAt).toISOString(),
|
|
63
|
+
completed_at: nowIso(),
|
|
64
|
+
duration_ms: Date.now() - startedAt,
|
|
65
|
+
config_source: config.source,
|
|
66
|
+
command_traces: gateResult.traces,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
writeJsonFileSync(path.join(cwd, FAILURES_FILE), failuresPayload);
|
|
70
|
+
writeJsonFileSync(path.join(cwd, RUN_METADATA_FILE), metadataPayload);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
status,
|
|
74
|
+
failuresPath: path.join(cwd, FAILURES_FILE),
|
|
75
|
+
metadataPath: path.join(cwd, RUN_METADATA_FILE),
|
|
76
|
+
runId,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
import { readJsonFileSync } from './fs-utils.js';
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
8
|
+
addFormats(ajv);
|
|
9
|
+
|
|
10
|
+
const schemaCache = new Map();
|
|
11
|
+
|
|
12
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const packageRoot = path.resolve(currentDir, '..');
|
|
14
|
+
|
|
15
|
+
function loadSchema(schemaPath) {
|
|
16
|
+
if (schemaCache.has(schemaPath)) {
|
|
17
|
+
return schemaCache.get(schemaPath);
|
|
18
|
+
}
|
|
19
|
+
const schema = readJsonFileSync(schemaPath);
|
|
20
|
+
const validate = ajv.compile(schema);
|
|
21
|
+
schemaCache.set(schemaPath, validate);
|
|
22
|
+
return validate;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateAgainstSchema(schemaFileName, payload) {
|
|
26
|
+
const schemaPath = path.join(packageRoot, 'schemas', schemaFileName);
|
|
27
|
+
const validate = loadSchema(schemaPath);
|
|
28
|
+
const valid = validate(payload);
|
|
29
|
+
return {
|
|
30
|
+
valid: Boolean(valid),
|
|
31
|
+
errors: validate.errors || [],
|
|
32
|
+
schemaPath,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
AGENT_BRIEF_JSON_FILE,
|
|
4
|
+
AGENT_BRIEF_MD_FILE,
|
|
5
|
+
DEFAULT_POLICY,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
import { readJsonFileSync, writeJsonFileSync, writeTextFileSync } from './fs-utils.js';
|
|
8
|
+
import { validateAgainstSchema } from './schema.js';
|
|
9
|
+
|
|
10
|
+
function scopeForFinding(finding) {
|
|
11
|
+
if (Array.isArray(finding.files) && finding.files.length === 1) return 'single_file';
|
|
12
|
+
if (finding.route && (!finding.files || finding.files.length <= 1)) return 'single_route';
|
|
13
|
+
return 'cross_route';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function actionForGate(gate) {
|
|
17
|
+
if (gate === 'lint') return 'Apply targeted lint fixes and re-run lint deterministically.';
|
|
18
|
+
if (gate === 'typecheck') return 'Resolve TypeScript errors for impacted files and re-run typecheck.';
|
|
19
|
+
if (gate === 'build') return 'Fix build-breaking code paths and confirm production build passes.';
|
|
20
|
+
return 'Reduce route-level performance/accessibility regressions and re-run lighthouse.';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createMarkdown(failures, brief) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push('# Quick Gate Agent Brief');
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(`Run: \`${failures.run_id}\``);
|
|
28
|
+
lines.push(`Mode: \`${failures.mode}\``);
|
|
29
|
+
lines.push(`Status: \`${failures.status}\``);
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('## Deterministic failures');
|
|
32
|
+
lines.push('');
|
|
33
|
+
|
|
34
|
+
if (failures.findings.length === 0) {
|
|
35
|
+
lines.push('- No deterministic failures detected.');
|
|
36
|
+
} else {
|
|
37
|
+
for (const finding of failures.findings) {
|
|
38
|
+
const routePart = finding.route ? ` (${finding.route})` : '';
|
|
39
|
+
lines.push(`- \`${finding.id}\`${routePart}: ${finding.summary} (actual: ${finding.actual}, threshold: ${finding.threshold})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push('## Priority actions');
|
|
45
|
+
lines.push('');
|
|
46
|
+
if (brief.priority_actions.length === 0) {
|
|
47
|
+
lines.push('- No actions required.');
|
|
48
|
+
} else {
|
|
49
|
+
for (const action of brief.priority_actions) {
|
|
50
|
+
const targets = action.target_files?.length ? ` targets: ${action.target_files.join(', ')}` : '';
|
|
51
|
+
lines.push(`- [${action.scope}] ${action.action}${targets}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('## Retry policy');
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(`- Max attempts: ${brief.retry_policy.max_attempts}`);
|
|
59
|
+
lines.push(`- Max patch lines: ${brief.retry_policy.max_patch_lines}`);
|
|
60
|
+
lines.push(`- Abort on no improvement after: ${brief.retry_policy.abort_on_no_improvement} attempt(s)`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push('## Escalation conditions');
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('- Escalate with reason code and evidence (`.quick-gate/failures.json`, `.quick-gate/run-metadata.json`) if unresolved.');
|
|
65
|
+
lines.push('- Stop when no-improvement cap, patch budget, or time cap is hit.');
|
|
66
|
+
|
|
67
|
+
return `${lines.join('\n')}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function executeSummarize({ input, cwd = process.cwd() }) {
|
|
71
|
+
const failures = readJsonFileSync(path.resolve(cwd, input));
|
|
72
|
+
|
|
73
|
+
const priorityActions = failures.findings.map((finding) => ({
|
|
74
|
+
finding_id: finding.id,
|
|
75
|
+
action: actionForGate(finding.gate),
|
|
76
|
+
scope: scopeForFinding(finding),
|
|
77
|
+
target_files: finding.files || [],
|
|
78
|
+
rationale: `${finding.gate} failed deterministically. Address this fact before any inferred optimizations.`,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const brief = {
|
|
82
|
+
run_id: failures.run_id,
|
|
83
|
+
mode: failures.mode,
|
|
84
|
+
status: failures.status,
|
|
85
|
+
summary:
|
|
86
|
+
failures.status === 'pass'
|
|
87
|
+
? 'All deterministic gates passed.'
|
|
88
|
+
: `${failures.findings.length} deterministic finding(s) require repair.`,
|
|
89
|
+
priority_actions: priorityActions,
|
|
90
|
+
retry_policy: {
|
|
91
|
+
max_attempts: DEFAULT_POLICY.maxAttempts,
|
|
92
|
+
max_patch_lines: DEFAULT_POLICY.maxPatchLines,
|
|
93
|
+
abort_on_no_improvement: DEFAULT_POLICY.abortOnNoImprovement,
|
|
94
|
+
},
|
|
95
|
+
escalation: failures.status === 'pass'
|
|
96
|
+
? { required: false }
|
|
97
|
+
: {
|
|
98
|
+
required: true,
|
|
99
|
+
reason_code: 'UNRESOLVED_DETERMINISTIC_FAILURES',
|
|
100
|
+
message: 'Escalate with evidence packet if bounded repair loop cannot clear deterministic failures.',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const validation = validateAgainstSchema('agent-brief.schema.json', brief);
|
|
105
|
+
if (!validation.valid) {
|
|
106
|
+
throw new Error(`agent-brief schema validation failed: ${JSON.stringify(validation.errors, null, 2)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const md = createMarkdown(failures, brief);
|
|
110
|
+
writeJsonFileSync(path.join(cwd, AGENT_BRIEF_JSON_FILE), brief);
|
|
111
|
+
writeTextFileSync(path.join(cwd, AGENT_BRIEF_MD_FILE), md);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
briefJsonPath: path.join(cwd, AGENT_BRIEF_JSON_FILE),
|
|
115
|
+
briefMdPath: path.join(cwd, AGENT_BRIEF_MD_FILE),
|
|
116
|
+
status: brief.status,
|
|
117
|
+
};
|
|
118
|
+
}
|