@gobing-ai/spur 0.4.0 → 0.4.2
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/config.global.yaml +4 -2
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/plugin-scripts.json +15 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/templates/AGENTS.md +1 -1
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +216 -57
- package/config/workflows/wrapup-pipeline.yaml +107 -14
- package/package.json +10 -10
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/super-planner.md +16 -6
- package/plugins/sp/agents/super-reviewer.md +14 -0
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +4 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +32 -15
- package/plugins/sp/hooks/context-owner.ts +56 -0
- package/plugins/sp/hooks/context-post-tool.ts +23 -0
- package/plugins/sp/hooks/context-session-start.ts +49 -27
- package/plugins/sp/hooks/context-session-stop.ts +131 -30
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
- package/plugins/sp/lib/idea-handoff.generated.mjs +43 -5
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
- package/plugins/sp/lib/inline-run.generated.mjs +351 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +150 -7
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +488 -0
- package/plugins/sp/scripts/batch-plan.ts +145 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +562 -80
- package/plugins/sp/scripts/inline-run-setup.ts +52 -78
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +30213 -61
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +86 -31
- package/plugins/sp/scripts/residual-scan.ts +28 -30
- package/plugins/sp/scripts/run-summary.mjs +432 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -11
- package/plugins/sp/scripts/session-timeline.ts +57 -19
- package/plugins/sp/scripts/task-diffstat.mjs +32 -29
- package/plugins/sp/scripts/task-diffstat.ts +34 -34
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +3 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
- package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +40 -7
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
- package/plugins/sp/skills/spur-cli/references/decision.md +16 -8
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +20 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +44 -12
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +42402 -26889
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
- package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
- package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
- package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
- package/web/_astro/channel.D4nG9YLf.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
- package/web/_astro/index.BhSwN8yY.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/commands/dev-parallel.md +0 -31
- package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
- package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
- package/web/_astro/channel.CUFdtWhm.js +0 -1
- package/web/_astro/index.Dg_3bOaJ.css +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
|
|
3
|
+
* Source of truth: packages/app/src/workflow/plan-projection.ts (task 1104).
|
|
4
|
+
*/
|
|
5
|
+
export declare interface PhasedPlanItem {
|
|
6
|
+
label: string;
|
|
7
|
+
id: string;
|
|
8
|
+
outcome: 'pending' | 'active' | 'completed' | 'skipped' | 'failed' | 'blocked' | 'unattempted';
|
|
9
|
+
note?: string;
|
|
10
|
+
text: string;
|
|
11
|
+
parent?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare interface BatchTask {
|
|
15
|
+
wbs: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function planLetter(index: number): string;
|
|
19
|
+
export declare function planChild(parent: string, index: number): string;
|
|
20
|
+
export declare function buildPhasedPlan(def: unknown): PhasedPlanItem[] | null;
|
|
21
|
+
export declare function validatePhaseTable(def: unknown): string[];
|
|
22
|
+
export declare function insertOnEntry(items: PhasedPlanItem[], stateId: string, def: unknown): PhasedPlanItem[];
|
|
23
|
+
export declare function buildBatchPlan(tasks: BatchTask[]): PhasedPlanItem[][];
|
|
24
|
+
export declare interface ScheduleTask {
|
|
25
|
+
wbs: string;
|
|
26
|
+
dependencies?: string[];
|
|
27
|
+
files?: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare interface ScheduleOverlap {
|
|
30
|
+
before: string;
|
|
31
|
+
after: string;
|
|
32
|
+
files: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare interface BatchSchedule {
|
|
35
|
+
order: string[];
|
|
36
|
+
waves: string[][];
|
|
37
|
+
cycle: string[] | null;
|
|
38
|
+
overlaps: ScheduleOverlap[];
|
|
39
|
+
mode: 'sequential' | 'parallel';
|
|
40
|
+
reason: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function scheduleBatch(tasks: ScheduleTask[], opts?: { concurrency?: number }): BatchSchedule;
|
|
43
|
+
export declare function taskPhaseChildren(taskLetter: string, plan: PhasedPlanItem[]): PhasedPlanItem[];
|
|
44
|
+
export declare function hostStatus(
|
|
45
|
+
outcome: 'pending' | 'active' | 'completed' | 'skipped' | 'failed' | 'blocked' | 'unattempted',
|
|
46
|
+
): 'completed' | 'in_progress' | 'pending';
|
|
47
|
+
export declare function hostText(item: PhasedPlanItem): string;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// packages/app/src/workflow/plan-projection.ts
|
|
2
|
+
function planLetter(index) {
|
|
3
|
+
if (!Number.isInteger(index) || index < 0 || index > 25) {
|
|
4
|
+
throw new Error(`plan letter out of range: ${index} (expected 0..25)`);
|
|
5
|
+
}
|
|
6
|
+
return String.fromCharCode(65 + index);
|
|
7
|
+
}
|
|
8
|
+
function planChild(parent, index) {
|
|
9
|
+
if (!Number.isInteger(index) || index < 0 || index > 8) {
|
|
10
|
+
throw new Error(`plan child index out of range: ${index} (expected 0..8 under ${parent})`);
|
|
11
|
+
}
|
|
12
|
+
if (parent.length !== 1 || !/[A-Z]/.test(parent)) {
|
|
13
|
+
throw new Error(`plan child parent must be a single letter: ${parent}`);
|
|
14
|
+
}
|
|
15
|
+
return `${parent}${index + 1}`;
|
|
16
|
+
}
|
|
17
|
+
var PREPARE_ID = "prepare";
|
|
18
|
+
var REPORT_ID = "report";
|
|
19
|
+
var PHASE_ID_PREFIX = "phase.";
|
|
20
|
+
var TASK_ID_PREFIX = "task.";
|
|
21
|
+
var WAVE_SIZE = 24;
|
|
22
|
+
var TITLE_CAP = 60;
|
|
23
|
+
function isStateMachine(def) {
|
|
24
|
+
return def.kind !== "transition-flow" && def.kind !== "dag";
|
|
25
|
+
}
|
|
26
|
+
function hasDisplay(s) {
|
|
27
|
+
return s.display !== undefined;
|
|
28
|
+
}
|
|
29
|
+
function resolvePhaseTitle(states, phase) {
|
|
30
|
+
for (const s of states) {
|
|
31
|
+
if (s.display?.phase === phase && s.display.phaseTitle !== undefined)
|
|
32
|
+
return s.display.phaseTitle;
|
|
33
|
+
}
|
|
34
|
+
return phase;
|
|
35
|
+
}
|
|
36
|
+
function childText(label, title, id) {
|
|
37
|
+
return id.startsWith("prepare.") || id.startsWith("task.") || id === PREPARE_ID || id === REPORT_ID ? `${label} ${title}` : `${label} ${title} · ${id}`;
|
|
38
|
+
}
|
|
39
|
+
function buildPhasedPlan(def) {
|
|
40
|
+
if (!isStateMachine(def))
|
|
41
|
+
return null;
|
|
42
|
+
const annotated = def.states.filter(hasDisplay);
|
|
43
|
+
if (annotated.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const items = [
|
|
46
|
+
{ label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare", text: "A Prepare" },
|
|
47
|
+
{
|
|
48
|
+
label: "A1",
|
|
49
|
+
id: "prepare.1",
|
|
50
|
+
outcome: "pending",
|
|
51
|
+
parent: "A",
|
|
52
|
+
title: "Quick readiness",
|
|
53
|
+
text: "A1 Quick readiness"
|
|
54
|
+
},
|
|
55
|
+
{ label: "A2", id: "prepare.2", outcome: "pending", parent: "A", title: "Prepare Git", text: "A2 Prepare Git" },
|
|
56
|
+
{
|
|
57
|
+
label: "A3",
|
|
58
|
+
id: "prepare.3",
|
|
59
|
+
outcome: "pending",
|
|
60
|
+
parent: "A",
|
|
61
|
+
title: "Publish plan",
|
|
62
|
+
text: "A3 Publish plan"
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
const phases = [];
|
|
66
|
+
for (const s of def.states) {
|
|
67
|
+
if (s.display !== undefined && !phases.includes(s.display.phase))
|
|
68
|
+
phases.push(s.display.phase);
|
|
69
|
+
}
|
|
70
|
+
for (const [phaseIndex, phase] of phases.entries()) {
|
|
71
|
+
const letter = planLetter(phaseIndex + 1);
|
|
72
|
+
const phaseTitle = resolvePhaseTitle(def.states, phase);
|
|
73
|
+
items.push({
|
|
74
|
+
label: letter,
|
|
75
|
+
id: `${PHASE_ID_PREFIX}${phase}`,
|
|
76
|
+
outcome: "pending",
|
|
77
|
+
title: phaseTitle,
|
|
78
|
+
text: `${letter} ${phaseTitle}`
|
|
79
|
+
});
|
|
80
|
+
let childIndex = 0;
|
|
81
|
+
for (const s of def.states) {
|
|
82
|
+
if (s.display?.phase !== phase || s.display.show === "on-entry")
|
|
83
|
+
continue;
|
|
84
|
+
const label = planChild(letter, childIndex++);
|
|
85
|
+
const title = s.display.title ?? s.id;
|
|
86
|
+
items.push({
|
|
87
|
+
label,
|
|
88
|
+
id: s.id,
|
|
89
|
+
outcome: "pending",
|
|
90
|
+
parent: letter,
|
|
91
|
+
title,
|
|
92
|
+
text: childText(label, title, s.id)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
98
|
+
function validatePhaseTable(def) {
|
|
99
|
+
if (!isStateMachine(def))
|
|
100
|
+
return [];
|
|
101
|
+
const annotated = def.states.filter(hasDisplay);
|
|
102
|
+
if (annotated.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
const errors = [];
|
|
105
|
+
const terminals = new Set(def.terminalStates ?? []);
|
|
106
|
+
for (const s of def.states) {
|
|
107
|
+
if (s.display === undefined && !terminals.has(s.id)) {
|
|
108
|
+
errors.push(`state "${s.id}" has no display phase — every non-terminal state must declare display when any state does`);
|
|
109
|
+
}
|
|
110
|
+
if (s.display !== undefined && terminals.has(s.id)) {
|
|
111
|
+
errors.push(`terminal state "${s.id}" must not declare display`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const phases = [];
|
|
115
|
+
for (const s of annotated) {
|
|
116
|
+
if (!phases.includes(s.display.phase))
|
|
117
|
+
phases.push(s.display.phase);
|
|
118
|
+
}
|
|
119
|
+
if (phases.length > 25) {
|
|
120
|
+
errors.push(`${phases.length} phases exceed the 25 plan letters (B-Z); split the workflow or drop phase rows`);
|
|
121
|
+
}
|
|
122
|
+
for (const phase of phases) {
|
|
123
|
+
const members = annotated.filter((s) => s.display.phase === phase);
|
|
124
|
+
if (members.length > 9) {
|
|
125
|
+
errors.push(`phase "${phase}" has ${members.length} states — at most 9 digit children fit under one letter`);
|
|
126
|
+
}
|
|
127
|
+
const titles = [...new Set(members.map((s) => s.display.phaseTitle).filter((t) => t !== undefined))];
|
|
128
|
+
if (titles.length > 1) {
|
|
129
|
+
errors.push(`phase "${phase}" has conflicting phaseTitle values: ${titles.map((t) => `"${t}"`).join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return errors;
|
|
133
|
+
}
|
|
134
|
+
function insertOnEntry(items, stateId, def) {
|
|
135
|
+
if (!isStateMachine(def))
|
|
136
|
+
return items;
|
|
137
|
+
if (items.some((i) => i.id === stateId))
|
|
138
|
+
return items;
|
|
139
|
+
const display = def.states.find((s) => s.id === stateId)?.display;
|
|
140
|
+
if (display === undefined || (def.terminalStates ?? []).includes(stateId))
|
|
141
|
+
return items;
|
|
142
|
+
const phaseRow = items.find((i) => i.id === `${PHASE_ID_PREFIX}${display.phase}`);
|
|
143
|
+
if (phaseRow === undefined)
|
|
144
|
+
return items;
|
|
145
|
+
const children = items.filter((i) => i.parent === phaseRow.label);
|
|
146
|
+
const label = planChild(phaseRow.label, children.length);
|
|
147
|
+
const title = display.title ?? stateId;
|
|
148
|
+
const item = {
|
|
149
|
+
label,
|
|
150
|
+
id: stateId,
|
|
151
|
+
outcome: "pending",
|
|
152
|
+
parent: phaseRow.label,
|
|
153
|
+
title,
|
|
154
|
+
text: childText(label, title, stateId)
|
|
155
|
+
};
|
|
156
|
+
const last = children.at(-1) ?? phaseRow;
|
|
157
|
+
const at = items.indexOf(last) + 1;
|
|
158
|
+
return [...items.slice(0, at), item, ...items.slice(at)];
|
|
159
|
+
}
|
|
160
|
+
function truncateTitle(name) {
|
|
161
|
+
if (name.length <= TITLE_CAP)
|
|
162
|
+
return name;
|
|
163
|
+
return `${name.slice(0, TITLE_CAP - 1).replace(/[\uD800-\uDBFF]$/, "")}…`;
|
|
164
|
+
}
|
|
165
|
+
function buildBatchPlan(tasks) {
|
|
166
|
+
const waves = [];
|
|
167
|
+
for (let offset = 0;offset < tasks.length; offset += WAVE_SIZE) {
|
|
168
|
+
const items = [
|
|
169
|
+
{ label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare batch", text: "A Prepare batch" },
|
|
170
|
+
{
|
|
171
|
+
label: "A1",
|
|
172
|
+
id: "prepare.1",
|
|
173
|
+
outcome: "pending",
|
|
174
|
+
parent: "A",
|
|
175
|
+
title: "Resolve and freeze task set",
|
|
176
|
+
text: "A1 Resolve and freeze task set"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: "A2",
|
|
180
|
+
id: "prepare.2",
|
|
181
|
+
outcome: "pending",
|
|
182
|
+
parent: "A",
|
|
183
|
+
title: "Order by dependencies",
|
|
184
|
+
text: "A2 Order by dependencies"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: "A3",
|
|
188
|
+
id: "prepare.3",
|
|
189
|
+
outcome: "pending",
|
|
190
|
+
parent: "A",
|
|
191
|
+
title: "Prepare Git",
|
|
192
|
+
text: "A3 Prepare Git"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
label: "A4",
|
|
196
|
+
id: "prepare.4",
|
|
197
|
+
outcome: "pending",
|
|
198
|
+
parent: "A",
|
|
199
|
+
title: "Publish plan",
|
|
200
|
+
text: "A4 Publish plan"
|
|
201
|
+
}
|
|
202
|
+
];
|
|
203
|
+
tasks.slice(offset, offset + WAVE_SIZE).forEach((task, i) => {
|
|
204
|
+
const label = planLetter(i + 1);
|
|
205
|
+
const title = `${task.wbs} ${truncateTitle(task.name)}`;
|
|
206
|
+
items.push({
|
|
207
|
+
label,
|
|
208
|
+
id: `${TASK_ID_PREFIX}${task.wbs}`,
|
|
209
|
+
outcome: "pending",
|
|
210
|
+
title,
|
|
211
|
+
text: `${label} ${title}`
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
items.push({ label: "Z", id: REPORT_ID, outcome: "pending", title: "Batch report", text: "Z Batch report" });
|
|
215
|
+
waves.push(items);
|
|
216
|
+
}
|
|
217
|
+
return waves;
|
|
218
|
+
}
|
|
219
|
+
var byWbs = (a, b) => a.localeCompare(b, "en", { numeric: true });
|
|
220
|
+
function kahnOrder(nodes, preds) {
|
|
221
|
+
const indegree = new Map(nodes.map((n) => [n, preds.get(n)?.size ?? 0]));
|
|
222
|
+
const succ = new Map(nodes.map((n) => [n, []]));
|
|
223
|
+
for (const [n, ps] of preds)
|
|
224
|
+
for (const p of ps)
|
|
225
|
+
succ.get(p)?.push(n);
|
|
226
|
+
const ready = nodes.filter((n) => indegree.get(n) === 0).sort(byWbs);
|
|
227
|
+
const order = [];
|
|
228
|
+
for (let n = ready.shift();n !== undefined; n = ready.shift()) {
|
|
229
|
+
order.push(n);
|
|
230
|
+
for (const s of succ.get(n) ?? []) {
|
|
231
|
+
const d = (indegree.get(s) ?? 0) - 1;
|
|
232
|
+
indegree.set(s, d);
|
|
233
|
+
if (d === 0) {
|
|
234
|
+
ready.push(s);
|
|
235
|
+
ready.sort(byWbs);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return order;
|
|
240
|
+
}
|
|
241
|
+
function findCycle(remaining, preds) {
|
|
242
|
+
const state = new Map;
|
|
243
|
+
const stack = [];
|
|
244
|
+
const visit = (n) => {
|
|
245
|
+
state.set(n, "visiting");
|
|
246
|
+
stack.push(n);
|
|
247
|
+
for (const p of [...preds.get(n) ?? []].sort(byWbs)) {
|
|
248
|
+
if (!remaining.has(p))
|
|
249
|
+
continue;
|
|
250
|
+
if (state.get(p) === "visiting")
|
|
251
|
+
return [...stack.slice(stack.indexOf(p)), p];
|
|
252
|
+
if (!state.has(p)) {
|
|
253
|
+
const cycle = visit(p);
|
|
254
|
+
if (cycle)
|
|
255
|
+
return cycle;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
stack.pop();
|
|
259
|
+
state.set(n, "done");
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
for (const n of [...remaining].sort(byWbs)) {
|
|
263
|
+
if (state.has(n))
|
|
264
|
+
continue;
|
|
265
|
+
const cycle = visit(n);
|
|
266
|
+
if (cycle)
|
|
267
|
+
return cycle;
|
|
268
|
+
}
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
function scheduleBatch(tasks, opts = {}) {
|
|
272
|
+
const byId = new Map;
|
|
273
|
+
for (const t of tasks)
|
|
274
|
+
if (!byId.has(t.wbs))
|
|
275
|
+
byId.set(t.wbs, t);
|
|
276
|
+
const nodes = [...byId.keys()].sort(byWbs);
|
|
277
|
+
const preds = new Map(nodes.map((n) => [n, new Set((byId.get(n)?.dependencies ?? []).filter((d) => byId.has(d)))]));
|
|
278
|
+
const depOrder = kahnOrder(nodes, preds);
|
|
279
|
+
if (depOrder.length < nodes.length) {
|
|
280
|
+
const cycle = findCycle(new Set(nodes.filter((n) => !depOrder.includes(n))), preds);
|
|
281
|
+
return {
|
|
282
|
+
order: [],
|
|
283
|
+
waves: [],
|
|
284
|
+
cycle,
|
|
285
|
+
overlaps: [],
|
|
286
|
+
mode: "sequential",
|
|
287
|
+
reason: `dependency cycle ${cycle.join(" → ")}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const files = new Map(nodes.map((n) => [n, new Set((byId.get(n)?.files ?? []).map(normalizeTaskPath))]));
|
|
291
|
+
const overlaps = [];
|
|
292
|
+
for (let i = 0;i < depOrder.length; i++) {
|
|
293
|
+
const before = depOrder[i];
|
|
294
|
+
const beforeFiles = files.get(before) ?? new Set;
|
|
295
|
+
for (const after of depOrder.slice(i + 1)) {
|
|
296
|
+
const shared = [...beforeFiles].filter((f) => files.get(after)?.has(f)).sort();
|
|
297
|
+
if (shared.length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
overlaps.push({ before, after, files: shared });
|
|
300
|
+
preds.get(after)?.add(before);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const order = kahnOrder(nodes, preds);
|
|
304
|
+
const level = new Map;
|
|
305
|
+
for (const n of order) {
|
|
306
|
+
let l = 0;
|
|
307
|
+
for (const p of preds.get(n) ?? [])
|
|
308
|
+
l = Math.max(l, (level.get(p) ?? 0) + 1);
|
|
309
|
+
level.set(n, l);
|
|
310
|
+
}
|
|
311
|
+
const waves = [];
|
|
312
|
+
for (const n of order) {
|
|
313
|
+
const l = level.get(n) ?? 0;
|
|
314
|
+
waves[l] = [...waves[l] ?? [], n].sort(byWbs);
|
|
315
|
+
}
|
|
316
|
+
const widest = Math.max(0, ...waves.map((w) => w.length));
|
|
317
|
+
const concurrency = opts.concurrency ?? 2;
|
|
318
|
+
let reason = `widest wave holds ${widest} independent tasks`;
|
|
319
|
+
if (nodes.length < 2)
|
|
320
|
+
reason = "fewer than two tasks";
|
|
321
|
+
else if (concurrency < 2)
|
|
322
|
+
reason = "concurrency is 1";
|
|
323
|
+
else if (widest < 2)
|
|
324
|
+
reason = "every wave holds one task (dependency or file-overlap chain)";
|
|
325
|
+
const mode = nodes.length >= 2 && concurrency >= 2 && widest >= 2 ? "parallel" : "sequential";
|
|
326
|
+
return { order, waves, cycle: null, overlaps, mode, reason };
|
|
327
|
+
}
|
|
328
|
+
function normalizeTaskPath(path) {
|
|
329
|
+
return path.trim().replace(/^\.\//, "");
|
|
330
|
+
}
|
|
331
|
+
function taskPhaseChildren(taskLetter, plan) {
|
|
332
|
+
if (!Array.isArray(plan))
|
|
333
|
+
return [];
|
|
334
|
+
return plan.filter((i) => i.id.startsWith(PHASE_ID_PREFIX) && i.label.length === 1).map((row, i) => {
|
|
335
|
+
const label = planChild(taskLetter, i);
|
|
336
|
+
return {
|
|
337
|
+
label,
|
|
338
|
+
id: row.id,
|
|
339
|
+
outcome: row.outcome,
|
|
340
|
+
parent: taskLetter,
|
|
341
|
+
title: row.title,
|
|
342
|
+
text: `${label} ${row.title}`
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function hostStatus(outcome) {
|
|
347
|
+
if (outcome === "completed")
|
|
348
|
+
return "completed";
|
|
349
|
+
if (outcome === "active")
|
|
350
|
+
return "in_progress";
|
|
351
|
+
return "pending";
|
|
352
|
+
}
|
|
353
|
+
function hostText(item) {
|
|
354
|
+
const hostDecided = item.outcome === "skipped" || item.outcome === "failed" || item.outcome === "unattempted" || item.outcome === "blocked";
|
|
355
|
+
return hostDecided ? `${item.text} [${item.outcome}]` : item.text;
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
validatePhaseTable,
|
|
359
|
+
taskPhaseChildren,
|
|
360
|
+
scheduleBatch,
|
|
361
|
+
planLetter,
|
|
362
|
+
planChild,
|
|
363
|
+
insertOnEntry,
|
|
364
|
+
hostText,
|
|
365
|
+
hostStatus,
|
|
366
|
+
buildPhasedPlan,
|
|
367
|
+
buildBatchPlan
|
|
368
|
+
};
|
|
@@ -20,6 +20,10 @@ export declare interface QualityGateEnv {
|
|
|
20
20
|
}
|
|
21
21
|
export declare interface QualityGateOptions {
|
|
22
22
|
cwd?: string;
|
|
23
|
+
recomputeFingerprint?: (
|
|
24
|
+
cwd: string,
|
|
25
|
+
env: QualityGateEnv,
|
|
26
|
+
) => { ok: true; digest: string } | { ok: false; error: string };
|
|
23
27
|
}
|
|
24
28
|
export declare interface QualityGateResult {
|
|
25
29
|
status: 'PASS' | 'FAIL';
|
|
@@ -75,12 +79,32 @@ export declare interface BuildReceiptInput {
|
|
|
75
79
|
export declare function buildReceipt(input: BuildReceiptInput): CheckReceipt;
|
|
76
80
|
export declare function receiptFailsAtDigest(receipt: CheckReceipt | null, currentDigest: string): boolean;
|
|
77
81
|
export declare function readReceiptStatus(receiptPath: string, currentDigest: string): ReceiptReadStatus;
|
|
82
|
+
export declare interface ReceiptReuseDecision {
|
|
83
|
+
reuse: boolean;
|
|
84
|
+
receiptStatus: ReceiptReadStatus;
|
|
85
|
+
refusal?: string;
|
|
86
|
+
recomputedDigest?: string;
|
|
87
|
+
}
|
|
88
|
+
export declare function resolveReceiptReuse(
|
|
89
|
+
receiptPath: string,
|
|
90
|
+
env: QualityGateEnv,
|
|
91
|
+
cwd: string,
|
|
92
|
+
recomputeFn?: (
|
|
93
|
+
cwd: string,
|
|
94
|
+
env: QualityGateEnv,
|
|
95
|
+
) => { ok: true; digest: string } | { ok: false; error: string },
|
|
96
|
+
): ReceiptReuseDecision;
|
|
97
|
+
export declare function recomputeGateProofFingerprint(
|
|
98
|
+
cwd: string,
|
|
99
|
+
env: QualityGateEnv,
|
|
100
|
+
): { ok: true; digest: string } | { ok: false; error: string };
|
|
78
101
|
export declare interface LightScope {
|
|
79
102
|
files: string[];
|
|
80
103
|
workspaces: string[];
|
|
81
104
|
tests: string[];
|
|
82
105
|
}
|
|
83
106
|
export declare function lightScope(changedFiles: string[], exists?: (p: string) => boolean): LightScope;
|
|
107
|
+
export declare const scopeFromFiles: typeof lightScope;
|
|
84
108
|
export declare function shQuote(arg: string): string;
|
|
85
109
|
export declare interface LightCheckPlan {
|
|
86
110
|
id: string;
|
|
@@ -96,7 +120,17 @@ export declare interface LightGateResult {
|
|
|
96
120
|
receiptFile: string;
|
|
97
121
|
}
|
|
98
122
|
export declare function runLightGate(env: QualityGateEnv, options?: QualityGateOptions): LightGateResult;
|
|
99
|
-
export declare
|
|
123
|
+
export declare interface DeferredGateResult {
|
|
124
|
+
status: 'DEFERRED' | 'FAIL';
|
|
125
|
+
light: LightGateResult;
|
|
126
|
+
statusFile: string;
|
|
127
|
+
}
|
|
128
|
+
export declare function runDeferredGate(env: QualityGateEnv, options?: QualityGateOptions): DeferredGateResult;
|
|
129
|
+
export declare function runShellCommand(
|
|
130
|
+
cmd: string,
|
|
131
|
+
cwd: string | undefined,
|
|
132
|
+
extraEnv?: Record<string, string>,
|
|
133
|
+
): { output: string; code: number };
|
|
100
134
|
export declare function runQualityGate(
|
|
101
135
|
mode: 'run' | 'recheck',
|
|
102
136
|
env: QualityGateEnv,
|