@gobing-ai/knowledge-kit 0.0.12 → 0.0.14
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/quality-gate.ts
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import {
|
|
7
|
+
appendFileSync,
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
mkdtempSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
var MAX_GATE_ATTEMPTS = 5;
|
|
22
|
+
var MAX_FINDINGS = 20;
|
|
23
|
+
var RETRY_DELAY_MS_DEFAULT = 1e4;
|
|
24
|
+
var RETRY_DELAY_MS_ENV = 'SPUR_QUALITY_GATE_RETRY_DELAY_MS';
|
|
25
|
+
var LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
|
|
26
|
+
var FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
27
|
+
function isTransientLock(attemptOutput) {
|
|
28
|
+
return LOCKED_PATTERN.test(attemptOutput);
|
|
29
|
+
}
|
|
30
|
+
function extractFindings(logText) {
|
|
31
|
+
const found = new Set();
|
|
32
|
+
for (const match of logText.matchAll(FINDINGS_PATTERN)) found.add(match[0]);
|
|
33
|
+
return [...found]
|
|
34
|
+
.sort()
|
|
35
|
+
.slice(0, MAX_FINDINGS)
|
|
36
|
+
.map((anchor) => `${anchor} `)
|
|
37
|
+
.join('');
|
|
38
|
+
}
|
|
39
|
+
function tailLines(text, count) {
|
|
40
|
+
const lines = text.split(`
|
|
41
|
+
`);
|
|
42
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
43
|
+
const tail = lines.slice(Math.max(0, lines.length - count));
|
|
44
|
+
return tail.length === 0
|
|
45
|
+
? ''
|
|
46
|
+
: `${tail.join(`
|
|
47
|
+
`)}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
function retryMessage(attempt) {
|
|
51
|
+
return `quality gate: database is locked; retrying (${attempt}/${MAX_GATE_ATTEMPTS}) in 10s
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
function retryDelayMs(env) {
|
|
55
|
+
const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? '', 10);
|
|
56
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
|
|
57
|
+
}
|
|
58
|
+
function gateSleep(ms) {
|
|
59
|
+
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
60
|
+
}
|
|
61
|
+
function runShellCommand(cmd, cwd) {
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), 'spur-quality-gate-'));
|
|
63
|
+
const path = join(dir, 'output');
|
|
64
|
+
const fd = openSync(path, 'w');
|
|
65
|
+
try {
|
|
66
|
+
const result = spawnSync('sh', ['-c', cmd], { cwd, stdio: ['ignore', fd, fd] });
|
|
67
|
+
if (result.error !== undefined) {
|
|
68
|
+
return {
|
|
69
|
+
output: `sh -c failed: ${result.error.message}
|
|
70
|
+
`,
|
|
71
|
+
code: 1,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { output: readFileSync(path, 'utf8'), code: result.status ?? 1 };
|
|
75
|
+
} finally {
|
|
76
|
+
closeSync(fd);
|
|
77
|
+
rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function runQualityGate(mode, env, options = {}) {
|
|
81
|
+
const cwd = options.cwd;
|
|
82
|
+
const runDir = join('.spur', 'run');
|
|
83
|
+
mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
|
|
84
|
+
const rel = (name) => join(runDir, `${env.wbs}${name}`);
|
|
85
|
+
const logFile = rel('-test-gate.log');
|
|
86
|
+
const findingsFile = rel('-test-gate.findings');
|
|
87
|
+
const statusFile = rel('-test-gate.status');
|
|
88
|
+
const attemptFile = rel('-test-fix-attempt');
|
|
89
|
+
const abs = (p) => (cwd ? join(cwd, p) : p);
|
|
90
|
+
writeFileSync(abs(logFile), '');
|
|
91
|
+
if (mode === 'run')
|
|
92
|
+
writeFileSync(
|
|
93
|
+
abs(attemptFile),
|
|
94
|
+
`0
|
|
95
|
+
`,
|
|
96
|
+
);
|
|
97
|
+
let gateRc = 0;
|
|
98
|
+
let gateAttempt = 0;
|
|
99
|
+
if (mode === 'recheck' && (env.gateProbeCmd ?? '').length > 0) {
|
|
100
|
+
const probe = runShellCommand(env.gateProbeCmd ?? '', cwd);
|
|
101
|
+
writeFileSync(abs(`${logFile}.probe`), probe.output);
|
|
102
|
+
gateRc = probe.code;
|
|
103
|
+
appendFileSync(abs(logFile), probe.output);
|
|
104
|
+
rmSync(abs(`${logFile}.probe`), { force: true });
|
|
105
|
+
}
|
|
106
|
+
if (gateRc === 0) {
|
|
107
|
+
const delayMs = retryDelayMs(env);
|
|
108
|
+
for (gateAttempt = 1; gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
|
|
109
|
+
const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
|
|
110
|
+
const attempt = runShellCommand(env.qualityGateCmd ?? '', cwd);
|
|
111
|
+
writeFileSync(abs(attemptLogPath), attempt.output);
|
|
112
|
+
const locked = isTransientLock(attempt.output);
|
|
113
|
+
appendFileSync(abs(logFile), attempt.output);
|
|
114
|
+
rmSync(abs(attemptLogPath), { force: true });
|
|
115
|
+
gateRc = attempt.code;
|
|
116
|
+
if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS) break;
|
|
117
|
+
const line = retryMessage(gateAttempt);
|
|
118
|
+
process.stdout.write(line);
|
|
119
|
+
appendFileSync(abs(logFile), line);
|
|
120
|
+
gateSleep(delayMs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (gateRc === 0) {
|
|
124
|
+
const bytes = existsSync(abs(logFile)) ? statSync(abs(logFile)).size : 0;
|
|
125
|
+
const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : '';
|
|
126
|
+
process.stdout.write(`quality gate PASS (attempts: ${attemptsLabel}; log: ${logFile}; bytes: ${bytes})
|
|
127
|
+
`);
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`quality gate FAIL \u2014 last 40 lines follow (full log: ${logFile})
|
|
130
|
+
`);
|
|
131
|
+
process.stdout.write(tailLines(readFileSync(abs(logFile), 'utf8'), 40));
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(abs(findingsFile), extractFindings(readFileSync(abs(logFile), 'utf8')));
|
|
134
|
+
const status = gateRc === 0 ? 'PASS' : 'FAIL';
|
|
135
|
+
writeFileSync(
|
|
136
|
+
abs(statusFile),
|
|
137
|
+
`${status}
|
|
138
|
+
`,
|
|
139
|
+
);
|
|
140
|
+
appendFileSync(
|
|
141
|
+
abs(logFile),
|
|
142
|
+
`proof-digest: ${env.proofDigest ?? ''}
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile };
|
|
146
|
+
}
|
|
147
|
+
var QUALITY_GATE_USAGE = 'usage: quality-gate.ts <run|recheck> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest)';
|
|
148
|
+
function main(argv, env = process.env) {
|
|
149
|
+
const mode = argv[0];
|
|
150
|
+
if (mode !== 'run' && mode !== 'recheck') {
|
|
151
|
+
process.stderr.write(`${QUALITY_GATE_USAGE}
|
|
152
|
+
`);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
if ((env.wbs ?? '').length === 0) {
|
|
156
|
+
process.stderr.write('quality-gate: env `wbs` is required\n');
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
runQualityGate(mode, env);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
process.exit(main(process.argv.slice(2)));
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
extractFindings,
|
|
166
|
+
FINDINGS_PATTERN,
|
|
167
|
+
isTransientLock,
|
|
168
|
+
LOCKED_PATTERN,
|
|
169
|
+
MAX_FINDINGS,
|
|
170
|
+
MAX_GATE_ATTEMPTS,
|
|
171
|
+
main,
|
|
172
|
+
QUALITY_GATE_USAGE,
|
|
173
|
+
RETRY_DELAY_MS_DEFAULT,
|
|
174
|
+
RETRY_DELAY_MS_ENV,
|
|
175
|
+
retryMessage,
|
|
176
|
+
runQualityGate,
|
|
177
|
+
runShellCommand,
|
|
178
|
+
tailLines,
|
|
179
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* quality-gate — shared retry/bounded-findings gate behind the task-pipeline test-gate
|
|
4
|
+
* wrappers (task 0823, feature I21, governance §1.2 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reproduces the former task-pipeline `test:onEnter` and `test-recheck:onEnter` shell
|
|
7
|
+
* programs one-for-one so the workflow stays inside the shell-program caps while
|
|
8
|
+
* writing the same `.spur/run` artifacts:
|
|
9
|
+
* - `<wbs>-test-gate.log` merged gate attempt output + `proof-digest:` trailer
|
|
10
|
+
* - `<wbs>-test-gate.findings` unique `file.ext:line` anchors, code-unit sorted, capped at 20
|
|
11
|
+
* - `<wbs>-test-gate.status` `PASS`/`FAIL`
|
|
12
|
+
* - `<wbs>-test-fix-attempt` remediation counter, reset to `0` by `run` only
|
|
13
|
+
*
|
|
14
|
+
* Modes:
|
|
15
|
+
* run reset the attempt counter, truncate the log, execute the gate loop
|
|
16
|
+
* recheck truncate the log, execute the probe first (probe failure is the gate
|
|
17
|
+
* failure, the gate loop is skipped), then execute the gate loop
|
|
18
|
+
*
|
|
19
|
+
* Retry contract: a failed attempt is retried (up to 5 total attempts) only when its
|
|
20
|
+
* output matches a SQLite busy/locked error; the delay defaults to 10 seconds and is
|
|
21
|
+
* overridable via `SPUR_QUALITY_GATE_RETRY_DELAY_MS` (tests).
|
|
22
|
+
*
|
|
23
|
+
* Environment: `wbs` (required), `qualityGateCmd`, `gateProbeCmd` (recheck), `proofDigest`.
|
|
24
|
+
* Soft-fail contract: the process always exits 0; the verdict lives in the status file.
|
|
25
|
+
*
|
|
26
|
+
* Node-builtin imports only; pure helpers are exported for unit testing (ADR-065).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import {
|
|
31
|
+
appendFileSync,
|
|
32
|
+
closeSync,
|
|
33
|
+
existsSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
mkdtempSync,
|
|
36
|
+
openSync,
|
|
37
|
+
readFileSync,
|
|
38
|
+
rmSync,
|
|
39
|
+
statSync,
|
|
40
|
+
writeFileSync,
|
|
41
|
+
} from 'node:fs';
|
|
42
|
+
import { tmpdir } from 'node:os';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
|
|
45
|
+
export const MAX_GATE_ATTEMPTS = 5;
|
|
46
|
+
export const MAX_FINDINGS = 20;
|
|
47
|
+
export const RETRY_DELAY_MS_DEFAULT = 10_000;
|
|
48
|
+
export const RETRY_DELAY_MS_ENV = 'SPUR_QUALITY_GATE_RETRY_DELAY_MS';
|
|
49
|
+
|
|
50
|
+
/** Transient SQLite lock/busy markers that justify a gate retry (same as the former grep -Eq). */
|
|
51
|
+
export const LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
|
|
52
|
+
|
|
53
|
+
/** `file.ext:line` anchor extraction, mirroring the former grep -oE. */
|
|
54
|
+
export const FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
55
|
+
|
|
56
|
+
export interface QualityGateEnv {
|
|
57
|
+
wbs: string;
|
|
58
|
+
qualityGateCmd?: string;
|
|
59
|
+
gateProbeCmd?: string;
|
|
60
|
+
proofDigest?: string;
|
|
61
|
+
[key: string]: string | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QualityGateOptions {
|
|
65
|
+
/** Base directory for `.spur/run`; defaults to the process cwd (CLI behavior). */
|
|
66
|
+
cwd?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface QualityGateResult {
|
|
70
|
+
status: 'PASS' | 'FAIL';
|
|
71
|
+
attempts: number;
|
|
72
|
+
logFile: string;
|
|
73
|
+
findingsFile: string;
|
|
74
|
+
statusFile: string;
|
|
75
|
+
attemptFile: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isTransientLock(attemptOutput: string): boolean {
|
|
79
|
+
return LOCKED_PATTERN.test(attemptOutput);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Unique anchors, code-unit sorted, capped at MAX_FINDINGS, each followed by one space. */
|
|
83
|
+
export function extractFindings(logText: string): string {
|
|
84
|
+
const found = new Set<string>();
|
|
85
|
+
for (const match of logText.matchAll(FINDINGS_PATTERN)) found.add(match[0]);
|
|
86
|
+
return [...found]
|
|
87
|
+
.sort()
|
|
88
|
+
.slice(0, MAX_FINDINGS)
|
|
89
|
+
.map((anchor) => `${anchor} `)
|
|
90
|
+
.join('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `tail -n <count>` on POSIX text: last count lines, newline-terminated. */
|
|
94
|
+
export function tailLines(text: string, count: number): string {
|
|
95
|
+
const lines = text.split('\n');
|
|
96
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
97
|
+
const tail = lines.slice(Math.max(0, lines.length - count));
|
|
98
|
+
return tail.length === 0 ? '' : `${tail.join('\n')}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function retryMessage(attempt: number): string {
|
|
102
|
+
return `quality gate: database is locked; retrying (${attempt}/${MAX_GATE_ATTEMPTS}) in 10s\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function retryDelayMs(env: QualityGateEnv): number {
|
|
106
|
+
const raw = Number.parseInt(env[RETRY_DELAY_MS_ENV] ?? '', 10);
|
|
107
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : RETRY_DELAY_MS_DEFAULT;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function gateSleep(ms: number): void {
|
|
111
|
+
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `sh -c <cmd> > <attempt log> 2>&1` equivalent: merged output plus exit code. */
|
|
115
|
+
export function runShellCommand(cmd: string, cwd: string | undefined): { output: string; code: number } {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), 'spur-quality-gate-'));
|
|
117
|
+
const path = join(dir, 'output');
|
|
118
|
+
const fd = openSync(path, 'w');
|
|
119
|
+
try {
|
|
120
|
+
// One file descriptor preserves stream order and avoids spawnSync's pipe buffer limit.
|
|
121
|
+
const result = spawnSync('sh', ['-c', cmd], { cwd, stdio: ['ignore', fd, fd] });
|
|
122
|
+
if (result.error !== undefined) {
|
|
123
|
+
return { output: `sh -c failed: ${result.error.message}\n`, code: 1 };
|
|
124
|
+
}
|
|
125
|
+
return { output: readFileSync(path, 'utf8'), code: result.status ?? 1 };
|
|
126
|
+
} finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function runQualityGate(
|
|
133
|
+
mode: 'run' | 'recheck',
|
|
134
|
+
env: QualityGateEnv,
|
|
135
|
+
options: QualityGateOptions = {},
|
|
136
|
+
): QualityGateResult {
|
|
137
|
+
const cwd = options.cwd;
|
|
138
|
+
const runDir = join('.spur', 'run');
|
|
139
|
+
mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
|
|
140
|
+
const rel = (name: string): string => join(runDir, `${env.wbs}${name}`);
|
|
141
|
+
const logFile = rel('-test-gate.log');
|
|
142
|
+
const findingsFile = rel('-test-gate.findings');
|
|
143
|
+
const statusFile = rel('-test-gate.status');
|
|
144
|
+
const attemptFile = rel('-test-fix-attempt');
|
|
145
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
146
|
+
|
|
147
|
+
// run: `echo 0 > "$ATTEMPT_FILE" && : > "$LOG_FILE"`; recheck: truncate only.
|
|
148
|
+
writeFileSync(abs(logFile), '');
|
|
149
|
+
if (mode === 'run') writeFileSync(abs(attemptFile), '0\n');
|
|
150
|
+
|
|
151
|
+
let gateRc = 0;
|
|
152
|
+
let gateAttempt = 0;
|
|
153
|
+
|
|
154
|
+
// recheck probe: a probe failure is the gate failure; the gate loop is skipped.
|
|
155
|
+
if (mode === 'recheck' && (env.gateProbeCmd ?? '').length > 0) {
|
|
156
|
+
const probe = runShellCommand(env.gateProbeCmd ?? '', cwd);
|
|
157
|
+
writeFileSync(abs(`${logFile}.probe`), probe.output);
|
|
158
|
+
gateRc = probe.code;
|
|
159
|
+
appendFileSync(abs(logFile), probe.output);
|
|
160
|
+
rmSync(abs(`${logFile}.probe`), { force: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (gateRc === 0) {
|
|
164
|
+
const delayMs = retryDelayMs(env);
|
|
165
|
+
for (gateAttempt = 1; gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
|
|
166
|
+
const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
|
|
167
|
+
const attempt = runShellCommand(env.qualityGateCmd ?? '', cwd);
|
|
168
|
+
writeFileSync(abs(attemptLogPath), attempt.output);
|
|
169
|
+
const locked = isTransientLock(attempt.output);
|
|
170
|
+
appendFileSync(abs(logFile), attempt.output);
|
|
171
|
+
rmSync(abs(attemptLogPath), { force: true });
|
|
172
|
+
gateRc = attempt.code;
|
|
173
|
+
if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS) break;
|
|
174
|
+
const line = retryMessage(gateAttempt);
|
|
175
|
+
process.stdout.write(line); // tee: stdout and the log
|
|
176
|
+
appendFileSync(abs(logFile), line);
|
|
177
|
+
gateSleep(delayMs);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (gateRc === 0) {
|
|
182
|
+
const bytes = existsSync(abs(logFile)) ? statSync(abs(logFile)).size : 0;
|
|
183
|
+
const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : '';
|
|
184
|
+
process.stdout.write(`quality gate PASS (attempts: ${attemptsLabel}; log: ${logFile}; bytes: ${bytes})\n`);
|
|
185
|
+
} else {
|
|
186
|
+
process.stdout.write(`quality gate FAIL — last 40 lines follow (full log: ${logFile})\n`);
|
|
187
|
+
process.stdout.write(tailLines(readFileSync(abs(logFile), 'utf8'), 40));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeFileSync(abs(findingsFile), extractFindings(readFileSync(abs(logFile), 'utf8')));
|
|
191
|
+
const status: 'PASS' | 'FAIL' = gateRc === 0 ? 'PASS' : 'FAIL';
|
|
192
|
+
writeFileSync(abs(statusFile), `${status}\n`);
|
|
193
|
+
appendFileSync(abs(logFile), `proof-digest: ${env.proofDigest ?? ''}\n`);
|
|
194
|
+
|
|
195
|
+
return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const QUALITY_GATE_USAGE =
|
|
199
|
+
'usage: quality-gate.ts <run|recheck> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest)';
|
|
200
|
+
|
|
201
|
+
export function main(argv: string[], env: QualityGateEnv = process.env): number {
|
|
202
|
+
const mode = argv[0];
|
|
203
|
+
if (mode !== 'run' && mode !== 'recheck') {
|
|
204
|
+
process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
if ((env.wbs ?? '').length === 0) {
|
|
208
|
+
process.stderr.write('quality-gate: env `wbs` is required\n');
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
runQualityGate(mode, env);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (import.meta.main) {
|
|
216
|
+
process.exit(main(process.argv.slice(2)));
|
|
217
|
+
}
|