@gobing-ai/spur 0.3.53 → 0.3.55
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/config/corpus-baseline.json +2783 -2783
- package/config/pipeline-budgets.json +42 -0
- package/config/plugin-scripts.json +73 -0
- package/config/workflow-composition-baseline.json +561 -0
- package/config/workflows/docs-pipeline.yaml +29 -13
- package/config/workflows/feature-dev.yaml +54 -2
- package/config/workflows/idea-pipeline.yaml +15 -0
- package/config/workflows/task-pipeline.yaml +77 -65
- package/config/workflows/wrapup-pipeline.yaml +40 -45
- package/package.json +9 -9
- package/plugins/README.md +1 -1
- package/plugins/sp/README.md +3 -3
- package/plugins/sp/agents/super-planner.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-history-load.md +1 -1
- package/plugins/sp/commands/dev-idea.md +10 -2
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +160 -0
- package/plugins/sp/scripts/batch-preflight.ts +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +479 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +75 -37
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +204 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +141 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +1 -1
- package/plugins/sp/scripts/feature-sync-bounded.mjs +290 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +9 -8
- package/plugins/sp/scripts/history-load.mjs +268 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +685 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -1
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/skills/daily-summary/SKILL.md +3 -3
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -5
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +3 -3
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -4
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -4
- package/spur.js +4270 -1323
- package/web/_astro/{BoardApp.plVXYkKo.js → BoardApp.DCLSB3Zs.js} +63 -63
- package/web/_astro/BoardApp.Dx5gzAhb.js +1 -0
- package/web/_astro/{TaskDetail.5DIyVQ_N.js → TaskDetail.DN_RxS-2.js} +1 -1
- package/web/_astro/{arc.BnUgL7ho.js → arc.CldTHRg-.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B9YMJau-.js → architectureDiagram-3BPJPVTR.DQ4T9oeU.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Dw2qgvWJ.js → blockDiagram-GPEHLZMM.CGDaXH8J.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.jTbU0-Nk.js → c4Diagram-AAUBKEIU.Cqsc2iub.js} +1 -1
- package/web/_astro/channel.CrBJYpxo.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Di46EtkU.js → chunk-2J33WTMH.CQSKsRmq.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DweHBwqQ.js → chunk-4BX2VUAB.Pv1HCtp8.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.rRjTKaa7.js → chunk-55IACEB6.-9MseOIk.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BkEQIkSV.js → chunk-727SXJPM.M3OI1DF7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.Cfe9IvAd.js → chunk-AQP2D5EJ.NfHka5Ca.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.fI6tLbeN.js → chunk-FMBD7UC4.B2suYe3s.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BIjgpq0O.js → chunk-ND2GUHAM.BfJ7aucP.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.ZwLnVxyz.js → chunk-QZHKN3VN._582hZVc.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BJAGYidE.js → classDiagram-4FO5ZUOK._tttU_jk.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BJAGYidE.js → classDiagram-v2-Q7XG4LA2._tttU_jk.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B5y3nz26.js → cose-bilkent-S5V4N54A.C4rVATLQ.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.DHmw0ZBC.js → dagre-BM42HDAG.8BLRi7f9.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.10gapJfY.js → diagram-2AECGRRQ.D6lwUZos.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Gh33_drM.js → diagram-5GNKFQAL.Cl_rMiVT.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.8ROWlfcX.js → diagram-KO2AKTUF.DhMExHLp.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BsP3tKmg.js → diagram-LMA3HP47.3OAxzH_o.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DM2WFDhU.js → diagram-OG6HWLK6.Uh9RLucG.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.royaC_lH.js → erDiagram-TEJ5UH35.izzElG0c.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C9GEvIie.js → flowDiagram-I6XJVG4X.BlGwp5qM.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CXO4T2U0.js → ganttDiagram-6RSMTGT7.je7Vf8dN.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Q02KzYH7.js → gitGraphDiagram-PVQCEYII.DNk0Ycop.js} +1 -1
- package/web/_astro/{index.DAxu50UF.css → index.V6Q7nhed.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DNfSDKJt.js → infoDiagram-5YYISTIA.K2HhGyWH.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BFttvtgJ.js → ishikawaDiagram-YF4QCWOH.BbbOW4IN.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Cfm-2SbG.js → journeyDiagram-JHISSGLW.B1UaZKqN.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BWGNW6fV.js → kanban-definition-UN3LZRKU.9tsw5QFd.js} +1 -1
- package/web/_astro/{linear.BprdUk2Y.js → linear.I9dvtu-j.js} +1 -1
- package/web/_astro/{mermaid.core.B2zRNxtX.js → mermaid.core.5bKJsMgf.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.g9jij8sw.js → mindmap-definition-RKZ34NQL.BC5MqSn6.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CbzR4n5b.js → pieDiagram-4H26LBE5.B8GIMyxD.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DxV3viWt.js → quadrantDiagram-W4KKPZXB.K_aZSM_u.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DvAb90JS.js → requirementDiagram-4Y6WPE33.B_V5kzfT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CIFz5t4D.js → sankeyDiagram-5OEKKPKP.DyV2quLS.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DzlEQynd.js → sequenceDiagram-3UESZ5HK.BlKvPnBv.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.Dph2V-ts.js → stateDiagram-AJRCARHV.C1lYUMdS.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.wJhfrLAy.js → stateDiagram-v2-BHNVJYJU.DJWjr3QV.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BrSiyubX.js → timeline-definition-PNZ67QCA.CYYiXVlv.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.COobPrUX.js → vennDiagram-CIIHVFJN.zYjQKa_S.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.CrjeTx8A.js → wardley-L42UT6IY.BGrWTY3D.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B8_fTQ8E.js → wardleyDiagram-YWT4CUSO.CkXxtcI1.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BPpokgVv.js → xychartDiagram-2RQKCTM6.BhKH3KG3.js} +1 -1
- package/web/index.html +2 -2
- package/config/workflows/planning-pipeline.yaml +0 -249
- package/config/workflows/task-pipeline2.yaml +0 -778
- package/web/_astro/BoardApp.pdtYu-yW.js +0 -1
- package/web/_astro/channel.DEqvaz-I.js +0 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - User annotations (learnings, issues, pending)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
11
12
|
import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
|
|
12
13
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
13
14
|
import { logger } from './logger';
|
|
@@ -149,26 +150,28 @@ Examples:
|
|
|
149
150
|
|
|
150
151
|
// ─── Date Helpers ─────────────────────────────────────────────────────────────
|
|
151
152
|
|
|
153
|
+
// ─── Date Helpers ─────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
152
155
|
/** Return today's date as YYYY-MM-DD in the system (git) timezone. */
|
|
153
156
|
export function todayLocal(): string {
|
|
154
157
|
// Spawn 'date' to get system timezone date, since JS runtime
|
|
155
158
|
// may have a different TZ (e.g. bun test forces UTC).
|
|
156
|
-
const proc =
|
|
157
|
-
return proc.stdout
|
|
159
|
+
const proc = spawnSync('date', ['+%Y-%m-%d'], { encoding: 'utf8' });
|
|
160
|
+
return (proc.stdout ?? '').trim();
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
/** Return yesterday's date as YYYY-MM-DD in the system (git) timezone. */
|
|
161
164
|
export function yesterdayLocal(): string {
|
|
162
165
|
// Use portable epoch math via date command
|
|
163
|
-
const epochProc =
|
|
164
|
-
const epoch = parseInt(epochProc.stdout
|
|
166
|
+
const epochProc = spawnSync('date', ['+%s'], { encoding: 'utf8' });
|
|
167
|
+
const epoch = parseInt((epochProc.stdout ?? '').trim(), 10);
|
|
165
168
|
const yesterdayEpoch = epoch - 86400;
|
|
166
169
|
// Try BSD -r first, then GNU -d @
|
|
167
|
-
let proc =
|
|
168
|
-
if (proc.
|
|
169
|
-
proc =
|
|
170
|
+
let proc = spawnSync('date', ['-r', String(yesterdayEpoch), '+%Y-%m-%d'], { encoding: 'utf8' });
|
|
171
|
+
if ((proc.status ?? 1) !== 0) {
|
|
172
|
+
proc = spawnSync('date', ['-d', `@${yesterdayEpoch}`, '+%Y-%m-%d'], { encoding: 'utf8' });
|
|
170
173
|
}
|
|
171
|
-
return proc.stdout
|
|
174
|
+
return (proc.stdout ?? '').trim();
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
export function getDateRange(dateStr: string): { start: string; end: string } {
|
|
@@ -181,40 +184,72 @@ export function getDateRange(dateStr: string): { start: string; end: string } {
|
|
|
181
184
|
return { start, end };
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
// ─── Subprocess Spawner ──────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export interface ProcessSpawnResult {
|
|
190
|
+
stdout: string;
|
|
191
|
+
stderr: string;
|
|
192
|
+
exitCode: number;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ProcessSpawner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => Promise<ProcessSpawnResult>;
|
|
196
|
+
|
|
197
|
+
export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
try {
|
|
200
|
+
const proc = spawn(cmd, args, {
|
|
201
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
202
|
+
env: env ?? process.env,
|
|
203
|
+
});
|
|
204
|
+
const stdoutChunks: Buffer[] = [];
|
|
205
|
+
const stderrChunks: Buffer[] = [];
|
|
206
|
+
proc.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
207
|
+
proc.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
208
|
+
proc.on('close', (exitCode) => {
|
|
209
|
+
resolve({
|
|
210
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
211
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
212
|
+
exitCode: exitCode ?? 0,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
proc.on('error', (err) => {
|
|
216
|
+
reject(err);
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
reject(err);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let processSpawner: ProcessSpawner = defaultProcessSpawner;
|
|
225
|
+
|
|
226
|
+
export function setProcessSpawner(next?: ProcessSpawner): void {
|
|
227
|
+
processSpawner = next ?? defaultProcessSpawner;
|
|
228
|
+
}
|
|
229
|
+
|
|
184
230
|
// ─── Ccusage Integration ─────────────────────────────────────────────────────
|
|
185
231
|
|
|
186
232
|
export async function getCcusageData(date: string): Promise<CcusageData | null> {
|
|
187
233
|
try {
|
|
188
234
|
// Check if ccusage is available
|
|
189
235
|
const env = { ...process.env };
|
|
190
|
-
const ccusageCheck =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
195
|
-
await new Response(ccusageCheck.stdout).text();
|
|
196
|
-
await ccusageCheck.exited;
|
|
236
|
+
const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
|
|
237
|
+
if (ccusageCheck.exitCode !== 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
197
240
|
|
|
198
241
|
// Get daily data for the date
|
|
199
242
|
const since = `${date}T00:00:00`;
|
|
200
243
|
const until = `${date}T23:59:59`;
|
|
201
244
|
|
|
202
|
-
const proc =
|
|
203
|
-
stdout: 'pipe',
|
|
204
|
-
stderr: 'pipe',
|
|
205
|
-
env,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const output = await new Response(proc.stdout).text();
|
|
209
|
-
const exitCode = await proc.exited;
|
|
245
|
+
const proc = await processSpawner('ccusage', ['daily', '--since', since, '--until', until, '--json'], env);
|
|
210
246
|
|
|
211
|
-
if (exitCode !== 0) {
|
|
212
|
-
|
|
213
|
-
logger.warn(`ccusage error: ${errorOutput}`);
|
|
247
|
+
if (proc.exitCode !== 0) {
|
|
248
|
+
logger.warn(`ccusage error: ${proc.stderr}`);
|
|
214
249
|
return null;
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
const data = JSON.parse(
|
|
252
|
+
const data = JSON.parse(proc.stdout) as CcusageData;
|
|
218
253
|
return data;
|
|
219
254
|
} catch (error) {
|
|
220
255
|
logger.warn(`Failed to get ccusage data: ${error}`);
|
|
@@ -228,21 +263,24 @@ export async function getGitCommits(date: string): Promise<GitCommit[]> {
|
|
|
228
263
|
try {
|
|
229
264
|
const { start, end } = getDateRange(date);
|
|
230
265
|
|
|
231
|
-
const proc =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
const proc = await processSpawner('git', [
|
|
267
|
+
'log',
|
|
268
|
+
'--since',
|
|
269
|
+
start,
|
|
270
|
+
'--until',
|
|
271
|
+
end,
|
|
272
|
+
'--pretty=format:%H|%ad|%s',
|
|
273
|
+
'--date=iso',
|
|
274
|
+
'--numstat',
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
if (proc.exitCode !== 0) {
|
|
240
278
|
logger.warn('Failed to get git commits');
|
|
241
279
|
return [];
|
|
242
280
|
}
|
|
243
281
|
|
|
244
282
|
const commits: GitCommit[] = [];
|
|
245
|
-
const lines =
|
|
283
|
+
const lines = proc.stdout.trim().split('\n');
|
|
246
284
|
|
|
247
285
|
let currentCommit: Partial<GitCommit> | null = null;
|
|
248
286
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts
|
|
6
|
+
var PIPELINE_TOKENS = [
|
|
7
|
+
"--next",
|
|
8
|
+
"dev-runall",
|
|
9
|
+
"dev-wrapall",
|
|
10
|
+
"dev-run",
|
|
11
|
+
"dev-wrap",
|
|
12
|
+
"dev-idea",
|
|
13
|
+
"runall",
|
|
14
|
+
"wrapall",
|
|
15
|
+
"run",
|
|
16
|
+
"wrap",
|
|
17
|
+
"idea"
|
|
18
|
+
];
|
|
19
|
+
var PIPELINE_DRIVING_REFUSE_MESSAGE = "⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)";
|
|
20
|
+
var MUTATING_FIX_REFUSE_MESSAGE = "⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)";
|
|
21
|
+
var IMPLEMENT_HEAVY_ADVISORY_MESSAGE = "⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides";
|
|
22
|
+
var IMPLEMENT_HEAVY_TOKENS = [
|
|
23
|
+
"dev-runall",
|
|
24
|
+
"dev-wrapall",
|
|
25
|
+
"dev-run",
|
|
26
|
+
"dev-wrap",
|
|
27
|
+
"dev-idea",
|
|
28
|
+
"runall",
|
|
29
|
+
"wrapall",
|
|
30
|
+
"run",
|
|
31
|
+
"wrap",
|
|
32
|
+
"idea",
|
|
33
|
+
"implement"
|
|
34
|
+
];
|
|
35
|
+
function hasMutatingFixMode(step) {
|
|
36
|
+
return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
|
|
37
|
+
}
|
|
38
|
+
function tokenMatches(testee, token) {
|
|
39
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i");
|
|
41
|
+
return re.test(testee);
|
|
42
|
+
}
|
|
43
|
+
function detectPipelineDriving(testee) {
|
|
44
|
+
if (typeof testee !== "string" || testee.length === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
|
|
48
|
+
}
|
|
49
|
+
function isImplementHeavyStep(step) {
|
|
50
|
+
if (typeof step !== "string" || step.length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
const nonMutatingOnly = tokenMatches(step, "dev-verify") || tokenMatches(step, "dev-review") || tokenMatches(step, "dev-unit");
|
|
53
|
+
const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
|
|
54
|
+
if (nonMutatingOnly && !hasMutating)
|
|
55
|
+
return false;
|
|
56
|
+
if (tokenMatches(step, "dev-refine") && !hasMutating && !tokenMatches(step, "--next")) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (tokenMatches(step, "dev-refine") && tokenMatches(step, "--next")) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return hasMutating;
|
|
63
|
+
}
|
|
64
|
+
function detectImplementHeavy(testee, derivedSteps = []) {
|
|
65
|
+
if (isImplementHeavyStep(testee))
|
|
66
|
+
return true;
|
|
67
|
+
if (!detectPipelineDriving(testee))
|
|
68
|
+
return false;
|
|
69
|
+
return derivedSteps.some((step) => isImplementHeavyStep(step));
|
|
70
|
+
}
|
|
71
|
+
function evaluateDogfoodGate(testee, options = {}) {
|
|
72
|
+
const maxRetryPresent = options.maxRetryPresent === true;
|
|
73
|
+
const steps = options.steps ?? [];
|
|
74
|
+
const pipelineDriving = detectPipelineDriving(testee);
|
|
75
|
+
const mutatingFix = hasMutatingFixMode(testee);
|
|
76
|
+
const implementHeavy = detectImplementHeavy(testee, steps);
|
|
77
|
+
if (pipelineDriving && !maxRetryPresent) {
|
|
78
|
+
return {
|
|
79
|
+
pipelineDriving,
|
|
80
|
+
mutatingFix,
|
|
81
|
+
maxRetryPresent,
|
|
82
|
+
implementHeavy,
|
|
83
|
+
refuse: true,
|
|
84
|
+
advisory: false,
|
|
85
|
+
message: PIPELINE_DRIVING_REFUSE_MESSAGE,
|
|
86
|
+
exitCode: 2
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (mutatingFix && !maxRetryPresent) {
|
|
90
|
+
return {
|
|
91
|
+
pipelineDriving,
|
|
92
|
+
mutatingFix,
|
|
93
|
+
maxRetryPresent,
|
|
94
|
+
implementHeavy,
|
|
95
|
+
refuse: true,
|
|
96
|
+
advisory: false,
|
|
97
|
+
message: MUTATING_FIX_REFUSE_MESSAGE,
|
|
98
|
+
exitCode: 2
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (implementHeavy) {
|
|
102
|
+
return {
|
|
103
|
+
pipelineDriving,
|
|
104
|
+
mutatingFix,
|
|
105
|
+
maxRetryPresent,
|
|
106
|
+
implementHeavy,
|
|
107
|
+
refuse: false,
|
|
108
|
+
advisory: true,
|
|
109
|
+
message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
|
|
110
|
+
exitCode: 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
pipelineDriving,
|
|
115
|
+
mutatingFix,
|
|
116
|
+
maxRetryPresent,
|
|
117
|
+
implementHeavy,
|
|
118
|
+
refuse: false,
|
|
119
|
+
advisory: false,
|
|
120
|
+
message: null,
|
|
121
|
+
exitCode: 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function parseCliArgs(argv) {
|
|
125
|
+
let testee = null;
|
|
126
|
+
let maxRetryPresent = false;
|
|
127
|
+
let steps = [];
|
|
128
|
+
let json = false;
|
|
129
|
+
let help = false;
|
|
130
|
+
for (let i = 0;i < argv.length; i++) {
|
|
131
|
+
const a = argv[i];
|
|
132
|
+
if (a === "--help" || a === "-h") {
|
|
133
|
+
help = true;
|
|
134
|
+
} else if (a === "--json") {
|
|
135
|
+
json = true;
|
|
136
|
+
} else if (a === "--max-retry-present") {
|
|
137
|
+
maxRetryPresent = true;
|
|
138
|
+
} else if (a === "--testee") {
|
|
139
|
+
testee = argv[++i] ?? null;
|
|
140
|
+
} else if (a === "--steps") {
|
|
141
|
+
const raw = argv[++i] ?? "";
|
|
142
|
+
steps = raw.length === 0 ? [] : raw.split("||").map((s) => s.trim()).filter(Boolean);
|
|
143
|
+
} else if (a === "--") {
|
|
144
|
+
testee = argv.slice(i + 1).join(" ");
|
|
145
|
+
break;
|
|
146
|
+
} else if (!a.startsWith("-") && testee === null) {
|
|
147
|
+
testee = a;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { testee, maxRetryPresent, steps, json, help };
|
|
151
|
+
}
|
|
152
|
+
var CLI_USAGE = `Usage:
|
|
153
|
+
bun plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts \\
|
|
154
|
+
--testee "<testee string>" [--max-retry-present] [--steps "s1||s2"] [--json]
|
|
155
|
+
|
|
156
|
+
Exit codes:
|
|
157
|
+
0 proceed (stdout may carry implement-heavy advisory)
|
|
158
|
+
2 refuse — pipeline-driving OR mutating --fix mode without --max-retry
|
|
159
|
+
1 usage error
|
|
160
|
+
|
|
161
|
+
Phase 1.0: run BEFORE deriving steps (omit --steps).
|
|
162
|
+
Phase 1 W8: re-run AFTER step derivation with --steps "label1||label2".`;
|
|
163
|
+
function runCli(argv) {
|
|
164
|
+
const { testee, maxRetryPresent, steps, json, help } = parseCliArgs(argv);
|
|
165
|
+
if (help) {
|
|
166
|
+
return { exitCode: 0, stdout: "", stderr: CLI_USAGE };
|
|
167
|
+
}
|
|
168
|
+
if (testee === null || testee.length === 0) {
|
|
169
|
+
return { exitCode: 1, stdout: "", stderr: CLI_USAGE };
|
|
170
|
+
}
|
|
171
|
+
const result = evaluateDogfoodGate(testee, { maxRetryPresent, steps });
|
|
172
|
+
if (json) {
|
|
173
|
+
return { exitCode: result.exitCode, stdout: `${JSON.stringify(result, null, 2)}
|
|
174
|
+
`, stderr: "" };
|
|
175
|
+
}
|
|
176
|
+
if (result.message) {
|
|
177
|
+
return { exitCode: result.exitCode, stdout: `${result.message}
|
|
178
|
+
`, stderr: "" };
|
|
179
|
+
}
|
|
180
|
+
return { exitCode: result.exitCode, stdout: "", stderr: "" };
|
|
181
|
+
}
|
|
182
|
+
{
|
|
183
|
+
const { exitCode, stdout, stderr } = runCli(process.argv.slice(2));
|
|
184
|
+
if (stdout)
|
|
185
|
+
process.stdout.write(stdout);
|
|
186
|
+
if (stderr)
|
|
187
|
+
process.stderr.write(`${stderr}
|
|
188
|
+
`);
|
|
189
|
+
process.exit(exitCode);
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
runCli,
|
|
193
|
+
parseCliArgs,
|
|
194
|
+
isImplementHeavyStep,
|
|
195
|
+
hasMutatingFixMode,
|
|
196
|
+
evaluateDogfoodGate,
|
|
197
|
+
detectPipelineDriving,
|
|
198
|
+
detectImplementHeavy,
|
|
199
|
+
PIPELINE_TOKENS,
|
|
200
|
+
PIPELINE_DRIVING_REFUSE_MESSAGE,
|
|
201
|
+
MUTATING_FIX_REFUSE_MESSAGE,
|
|
202
|
+
IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
|
|
203
|
+
CLI_USAGE
|
|
204
|
+
};
|
|
@@ -356,7 +356,7 @@ export function runCli(argv: string[]): { exitCode: number; stdout: string; stde
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
if (import.meta.main) {
|
|
359
|
-
const { exitCode, stdout, stderr } = runCli(
|
|
359
|
+
const { exitCode, stdout, stderr } = runCli(process.argv.slice(2));
|
|
360
360
|
if (stdout) process.stdout.write(stdout);
|
|
361
361
|
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
362
362
|
process.exit(exitCode);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// plugins/sp/scripts/dogfood-testing/validate-report.ts
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
var REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6];
|
|
8
|
+
var CANONICAL_PROTOCOL = "sp:dogfood-testing@1.2";
|
|
9
|
+
function countSectionHeadings(markdown, section) {
|
|
10
|
+
const re = new RegExp(`^### ${section}\\.`, "gm");
|
|
11
|
+
return (markdown.match(re) ?? []).length;
|
|
12
|
+
}
|
|
13
|
+
function countLedgerDataRows(markdown) {
|
|
14
|
+
const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
|
|
15
|
+
if (!heading || heading.index === undefined)
|
|
16
|
+
return null;
|
|
17
|
+
const after = markdown.slice(heading.index + heading[0].length);
|
|
18
|
+
const nextHeading = after.search(/^### /m);
|
|
19
|
+
const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
20
|
+
const rows = body.split(`
|
|
21
|
+
`).filter((line) => line.trim().startsWith("|")).filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim())).filter((line) => !/^\|\s*drift:/.test(line.trim()));
|
|
22
|
+
return Math.max(rows.length - 1, 0);
|
|
23
|
+
}
|
|
24
|
+
function declaredExecutedSteps(markdown) {
|
|
25
|
+
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
26
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
27
|
+
}
|
|
28
|
+
function validateReport(markdown) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (!markdown.includes("── Dogfood Summary ──"))
|
|
31
|
+
errors.push("missing_footer");
|
|
32
|
+
if (!markdown.includes("[Live:"))
|
|
33
|
+
errors.push("missing_live_path");
|
|
34
|
+
if (!markdown.includes("[Report:"))
|
|
35
|
+
errors.push("missing_report_path");
|
|
36
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
37
|
+
const count = countSectionHeadings(markdown, section);
|
|
38
|
+
if (count === 0)
|
|
39
|
+
errors.push(`missing_section:${section}`);
|
|
40
|
+
if (count > 1)
|
|
41
|
+
errors.push(`duplicate_section:${section}`);
|
|
42
|
+
}
|
|
43
|
+
if (!markdown.includes("#### Fixed") || !markdown.includes("#### Unresolved")) {
|
|
44
|
+
errors.push("missing_issues_subheads");
|
|
45
|
+
}
|
|
46
|
+
const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
|
|
47
|
+
if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
|
|
48
|
+
errors.push("protocol_string");
|
|
49
|
+
}
|
|
50
|
+
const executed = declaredExecutedSteps(markdown);
|
|
51
|
+
if (executed === null) {
|
|
52
|
+
errors.push("missing_steps_declared");
|
|
53
|
+
} else {
|
|
54
|
+
const rows = countLedgerDataRows(markdown);
|
|
55
|
+
if (rows === null || rows !== executed) {
|
|
56
|
+
errors.push("ledger_cardinality");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { ok: errors.length === 0, errors };
|
|
60
|
+
}
|
|
61
|
+
function parseValidateCliArgs(argv) {
|
|
62
|
+
let file = null;
|
|
63
|
+
let json = false;
|
|
64
|
+
let help = false;
|
|
65
|
+
for (let i = 0;i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === "--help" || a === "-h")
|
|
68
|
+
help = true;
|
|
69
|
+
else if (a === "--json")
|
|
70
|
+
json = true;
|
|
71
|
+
else if (a === "--file")
|
|
72
|
+
file = argv[++i] ?? null;
|
|
73
|
+
else if (!a.startsWith("-") && file === null)
|
|
74
|
+
file = a;
|
|
75
|
+
}
|
|
76
|
+
return { file, json, help };
|
|
77
|
+
}
|
|
78
|
+
var VALIDATE_CLI_USAGE = `Usage:
|
|
79
|
+
bun plugins/sp/scripts/dogfood-testing/validate-report.ts --file <report.md> [--json]
|
|
80
|
+
|
|
81
|
+
Exit codes:
|
|
82
|
+
0 report validates clean (complete-report shape)
|
|
83
|
+
2 validation failed (errors on stdout / --json)
|
|
84
|
+
1 usage error
|
|
85
|
+
|
|
86
|
+
Phase 4 finalize MUST run this before status: complete (task 0278 R6).
|
|
87
|
+
On exit 2: set status: aborted and list error codes under #### Unresolved.`;
|
|
88
|
+
function runValidateCli(argv, readFile) {
|
|
89
|
+
const { file, json, help } = parseValidateCliArgs(argv);
|
|
90
|
+
if (help)
|
|
91
|
+
return { exitCode: 0, stdout: "", stderr: VALIDATE_CLI_USAGE };
|
|
92
|
+
if (file === null || file.length === 0) {
|
|
93
|
+
return { exitCode: 1, stdout: "", stderr: VALIDATE_CLI_USAGE };
|
|
94
|
+
}
|
|
95
|
+
let markdown;
|
|
96
|
+
try {
|
|
97
|
+
markdown = readFile(file);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return { exitCode: 1, stdout: "", stderr: `Failed to read ${file}: ${msg}` };
|
|
101
|
+
}
|
|
102
|
+
const result = validateReport(markdown);
|
|
103
|
+
if (json) {
|
|
104
|
+
return {
|
|
105
|
+
exitCode: result.ok ? 0 : 2,
|
|
106
|
+
stdout: `${JSON.stringify(result, null, 2)}
|
|
107
|
+
`,
|
|
108
|
+
stderr: ""
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
return { exitCode: 0, stdout: `ok
|
|
113
|
+
`, stderr: "" };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
exitCode: 2,
|
|
117
|
+
stdout: `${result.errors.join(`
|
|
118
|
+
`)}
|
|
119
|
+
`,
|
|
120
|
+
stderr: ""
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function mainCli(argv = process.argv.slice(2)) {
|
|
124
|
+
const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, "utf8"));
|
|
125
|
+
if (stdout)
|
|
126
|
+
process.stdout.write(stdout);
|
|
127
|
+
if (stderr)
|
|
128
|
+
process.stderr.write(`${stderr}
|
|
129
|
+
`);
|
|
130
|
+
return exitCode;
|
|
131
|
+
}
|
|
132
|
+
{
|
|
133
|
+
process.exit(mainCli());
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
validateReport,
|
|
137
|
+
runValidateCli,
|
|
138
|
+
parseValidateCliArgs,
|
|
139
|
+
mainCli,
|
|
140
|
+
VALIDATE_CLI_USAGE
|
|
141
|
+
};
|
|
@@ -156,7 +156,7 @@ export function runValidateCli(
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/** CLI entry for Phase 4 self-validate (import.meta.main). */
|
|
159
|
-
export function mainCli(argv: string[] =
|
|
159
|
+
export function mainCli(argv: string[] = process.argv.slice(2)): number {
|
|
160
160
|
const { exitCode, stdout, stderr } = runValidateCli(argv, (p) => readFileSync(p, 'utf8'));
|
|
161
161
|
if (stdout) process.stdout.write(stdout);
|
|
162
162
|
if (stderr) process.stderr.write(`${stderr}\n`);
|