opencode-swarm 7.189.0 → 7.190.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -6
- package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/agents/architect.d.ts +1 -1
- package/dist/background/completion-observer.d.ts +6 -0
- package/dist/cli/{auto-checkpoint-m7c4x8bk.js → auto-checkpoint-qvk04fya.js} +9 -9
- package/dist/cli/{checkpoint-m7q9886q.js → checkpoint-9q2r5c3j.js} +18 -20
- package/dist/cli/{coder-settlement-hh81z2f7.js → coder-settlement-xcparbf3.js} +13 -13
- package/dist/cli/{config-doctor-0rexy3xt.js → config-doctor-by0ak49m.js} +23 -20
- package/dist/cli/{core-ays6stz4.js → core-b1b3kxzy.js} +4 -4
- package/dist/cli/{curation-policy-65ckmmd5.js → curation-policy-6p6fcfjb.js} +6 -6
- package/dist/cli/{curator-dfzkfgs0.js → curator-d7b88mzr.js} +44 -44
- package/dist/cli/{curator-drift-nts5atks.js → curator-drift-6v604220.js} +9 -9
- package/dist/cli/curator-llm-factory-yvbktznk.js +82 -0
- package/dist/cli/{doctor-knowledge-receipt-phase-id-ec40k5hn.js → doctor-knowledge-receipt-phase-id-kwrvnps9.js} +9 -9
- package/dist/cli/{evidence-summary-service-vgtm72g5.js → evidence-summary-service-mzr2q4bn.js} +21 -22
- package/dist/cli/{gate-evidence-vmn65kxe.js → gate-evidence-78jes9kq.js} +8 -8
- package/dist/cli/guardrail-explain-8kt2mxmw.js +83 -0
- package/dist/cli/{guardrail-log-hc8fjm4x.js → guardrail-log-2sthykag.js} +8 -8
- package/dist/cli/{guardrail-reset-ct00w300.js → guardrail-reset-2n746rzv.js} +44 -44
- package/dist/cli/{hive-promoter-2xax6van.js → hive-promoter-s7a5gwhz.js} +44 -44
- package/dist/cli/{index-cbqkag7q.js → index-0qc7a88d.js} +3 -3
- package/dist/cli/{index-g03m5jhw.js → index-1s95jrgc.js} +3 -3
- package/dist/cli/{index-9tvjwv01.js → index-2fpv2wmc.js} +1 -1
- package/dist/cli/{index-2btybj8w.js → index-2kpqqcch.js} +4 -4
- package/dist/cli/{index-3dx0c7ve.js → index-3t9t9q2q.js} +44 -44
- package/dist/cli/{index-8q8783g0.js → index-3zwhxk98.js} +1 -1
- package/dist/cli/{index-34vsyj15.js → index-49e65y51.js} +5 -5
- package/dist/cli/{index-5v09zw1q.js → index-69tgdm9t.js} +18 -8
- package/dist/cli/{index-d3rjvm9e.js → index-7vnr5cmp.js} +2 -2
- package/dist/cli/{index-twb3f4tk.js → index-8bx6vhzk.js} +1 -1
- package/dist/cli/{index-nhsafbrh.js → index-9gcqmgbc.js} +5 -5
- package/dist/cli/{index-4p3jh14t.js → index-9xxqdzp4.js} +4 -4
- package/dist/cli/{index-qx3g1bbe.js → index-a5ca62vt.js} +1 -1
- package/dist/cli/{index-bskex45z.js → index-ajzgeb2s.js} +2 -2
- package/dist/cli/{index-8xabhfwr.js → index-b2j231jy.js} +2 -2
- package/dist/cli/{index-cj4mp0fm.js → index-dmq7p8s9.js} +6 -6
- package/dist/cli/{index-bzbxn24k.js → index-ej1hdw16.js} +5 -5
- package/dist/cli/{index-waxvpwbv.js → index-ejk34dnw.js} +127 -13
- package/dist/cli/{index-skdpgjar.js → index-fb5s32c2.js} +862 -1221
- package/dist/cli/{index-ntda6tje.js → index-feaxsjrt.js} +10 -2
- package/dist/cli/{index-2nexf5vz.js → index-ffgcdf49.js} +22147 -15396
- package/dist/cli/{index-kqegvngt.js → index-h0xhec3d.js} +4 -4
- package/dist/cli/{index-kaqdzhj3.js → index-kfevm0n5.js} +2 -2
- package/dist/cli/{index-4a7j5f60.js → index-kp8k95v9.js} +4 -4
- package/dist/cli/{index-x1eh2636.js → index-mcv0v2dp.js} +1 -1
- package/dist/cli/{index-9czp2dcg.js → index-mjfpaxw3.js} +7 -7
- package/dist/cli/{index-cdjxzs2r.js → index-mp1s7b0n.js} +7 -7
- package/dist/cli/{index-z29hksyw.js → index-mseerehd.js} +26 -10
- package/dist/cli/{index-f6exny9m.js → index-n8q43eff.js} +2 -2
- package/dist/cli/{index-7hr5m7yp.js → index-nkgzkt7c.js} +1 -1
- package/dist/cli/{index-9j3ybyfs.js → index-npe10823.js} +7 -7
- package/dist/cli/{index-16m2m4ah.js → index-p69k8v0e.js} +1 -1
- package/dist/cli/{index-p2tqzwh6.js → index-qs9myayh.js} +5 -10
- package/dist/cli/{index-hh7pa3yk.js → index-r9yvpr2m.js} +2 -2
- package/dist/cli/{index-w4zscz6p.js → index-rgkzcw2a.js} +5 -5
- package/dist/cli/{index-zfbxfm2m.js → index-s8b67e6h.js} +33 -12
- package/dist/cli/{index-9gqb5nkj.js → index-s92a5ta3.js} +4 -4
- package/dist/cli/{index-89f56ej4.js → index-sm01zpqe.js} +46 -46
- package/dist/cli/{index-q9kefh33.js → index-sxj143a4.js} +1 -1
- package/dist/cli/{index-1cwmj1gs.js → index-t0gj8s46.js} +77 -16
- package/dist/cli/{index-0gejy4cx.js → index-t73adn9b.js} +2 -2
- package/dist/cli/{index-fbgjj7h5.js → index-tkhenmms.js} +101 -13
- package/dist/cli/{index-zsrtt42s.js → index-v3bmwmqw.js} +25 -1
- package/dist/cli/{index-wqa31xnf.js → index-vhn4e6rm.js} +7 -7
- package/dist/cli/{index-5cfhwd65.js → index-xyvrfram.js} +2 -2
- package/dist/cli/{index-k8sarme1.js → index-y9v41185.js} +1 -1
- package/dist/cli/{index-1xa8wq76.js → index-z3ahy9c2.js} +2 -2
- package/dist/cli/{index-wrdktgpy.js → index-zk6ttjkt.js} +2 -2
- package/dist/cli/{index-mfxef20k.js → index-zyg1m9d0.js} +4 -4
- package/dist/cli/index.js +45 -45
- package/dist/cli/{knowledge-escalator-v3c8w86g.js → knowledge-escalator-n1ezv1zp.js} +15 -15
- package/dist/cli/{knowledge-events-kagaccj9.js → knowledge-events-nyqk3mr7.js} +13 -13
- package/dist/cli/{knowledge-link-h4pdcnya.js → knowledge-link-6hw3z6wn.js} +5 -5
- package/dist/cli/{knowledge-store-ywxjk5pb.js → knowledge-store-mnxqcyet.js} +6 -6
- package/dist/cli/{knowledge-validator-210gkpec.js → knowledge-validator-z30113mp.js} +8 -8
- package/dist/cli/{mcp-58hgx6av.js → mcp-mnbqw2s5.js} +1 -1
- package/dist/cli/{model-preflight-e69b48n0.js → model-preflight-ex482fsy.js} +44 -44
- package/dist/cli/{pending-delegations-g24d3qa9.js → pending-delegations-j18afz5z.js} +12 -12
- package/dist/cli/{pr-subscriptions-qprh2zsm.js → pr-subscriptions-yngda8mz.js} +12 -12
- package/dist/cli/{pr-workflow-gate-pj7dhkv4.js → pr-workflow-gate-0vt614f0.js} +44 -44
- package/dist/cli/{runner-4hfmp1ag.js → runner-ssnsje35.js} +6 -6
- package/dist/cli/{scan-cursor-s5wsxxd1.js → scan-cursor-8pwfg5g9.js} +7 -7
- package/dist/cli/{schema-smgngq9m.js → schema-e58ytmqt.js} +14 -2
- package/dist/cli/{scope-persistence-tbv2ar4w.js → scope-persistence-9qfx6ext.js} +16 -16
- package/dist/cli/{server-7p2ds5w2.js → server-wxcfwsdx.js} +45 -45
- package/dist/cli/{skill-generator-r4hsne71.js → skill-generator-tbd4tq9r.js} +17 -17
- package/dist/cli/{snapshot-coordination-init-ydjwh56d.js → snapshot-coordination-init-sckyzb94.js} +44 -44
- package/dist/cli/{speckit-checkoff-aj75vmjq.js → speckit-checkoff-k5jm0abh.js} +5 -5
- package/dist/cli/{telemetry-kvhe94gs.js → telemetry-9szfdfqv.js} +1 -1
- package/dist/cli/{workspace-snapshot-30kdf018.js → workspace-snapshot-9f12p1s1.js} +3 -3
- package/dist/cli/{worktree-collision-ownership-0bxj9f0t.js → worktree-collision-ownership-3wwfc72t.js} +14 -14
- package/dist/cli/{worktree-isolation-h0c22pm2.js → worktree-isolation-zfk7zk6a.js} +46 -44
- package/dist/commands/coupling.d.ts +21 -4
- package/dist/commands/epic-learning.d.ts +28 -0
- package/dist/commands/epic.d.ts +81 -36
- package/dist/commands/registry.d.ts +8 -8
- package/dist/commands/turbo.d.ts +16 -1
- package/dist/config/constants.d.ts +11 -1
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/schema.d.ts +291 -77
- package/dist/db/coordination-store.d.ts +6 -0
- package/dist/epic/close.d.ts +209 -0
- package/dist/{turbo/epic → epic}/cochange-conflict.d.ts +5 -4
- package/dist/{turbo/epic → epic}/cochange-source.d.ts +10 -1
- package/dist/epic/components.d.ts +238 -0
- package/dist/epic/config-gate.d.ts +25 -0
- package/dist/epic/config.d.ts +101 -0
- package/dist/{turbo/epic → epic}/coupling-report.d.ts +3 -1
- package/dist/epic/declared-scopes.d.ts +37 -0
- package/dist/epic/epic-branch.d.ts +157 -0
- package/dist/epic/gate-policy.d.ts +114 -0
- package/dist/epic/git-once.d.ts +26 -0
- package/dist/epic/learning-store.d.ts +176 -0
- package/dist/epic/learning.d.ts +198 -0
- package/dist/epic/legacy-migration.d.ts +26 -0
- package/dist/epic/lifecycle.d.ts +437 -0
- package/dist/epic/markers.d.ts +123 -0
- package/dist/epic/merge-epoch.d.ts +90 -0
- package/dist/epic/next-wave-format.d.ts +101 -0
- package/dist/epic/next-wave.d.ts +102 -0
- package/dist/epic/phase-readiness.d.ts +221 -0
- package/dist/epic/plan-key.d.ts +104 -0
- package/dist/epic/plan-shaping-seam.d.ts +112 -0
- package/dist/epic/planning-signals.d.ts +38 -0
- package/dist/epic/report.d.ts +152 -0
- package/dist/epic/residue-commit.d.ts +151 -0
- package/dist/epic/scorecard.d.ts +130 -0
- package/dist/epic/shaping-sizing.d.ts +108 -0
- package/dist/epic/shaping-suggestions.d.ts +172 -0
- package/dist/epic/shaping.d.ts +112 -0
- package/dist/epic/sizing.d.ts +60 -0
- package/dist/epic/start.d.ts +187 -0
- package/dist/epic/task-landing.d.ts +83 -0
- package/dist/epic/wave-close.d.ts +112 -0
- package/dist/epic/wave-select.d.ts +95 -0
- package/dist/evaluation/contracts.d.ts +1 -1
- package/dist/evaluation/public-api.d.ts +1 -1
- package/dist/full-auto/policy.d.ts +12 -1
- package/dist/hooks/delegation-gate/worktree-isolation.d.ts +16 -0
- package/dist/hooks/delegation-gate.d.ts +11 -0
- package/dist/hooks/guardrails/index.d.ts +7 -0
- package/dist/hooks/promotion-evidence-store.d.ts +2 -2
- package/dist/index.js +814 -807
- package/dist/plan/manager.d.ts +6 -8
- package/dist/plan/parallel-verdict.d.ts +13 -2
- package/dist/retention/caps.d.ts +1 -1
- package/dist/review/contracts.d.ts +8 -1
- package/dist/services/config-doctor.d.ts +38 -0
- package/dist/session/snapshot-reader.d.ts +2 -0
- package/dist/session/snapshot-writer.d.ts +13 -2
- package/dist/state.d.ts +13 -18
- package/dist/tools/epic-next-wave.d.ts +20 -0
- package/dist/tools/epic-phase-review.d.ts +43 -0
- package/dist/tools/index.d.ts +2 -3
- package/dist/tools/manifest.d.ts +2 -3
- package/dist/tools/phase-complete.d.ts +5 -1
- package/dist/tools/save-plan.d.ts +7 -0
- package/dist/tools/tool-metadata.d.ts +4 -8
- package/dist/worktree/merge.d.ts +14 -1
- package/opencode-swarm.schema.json +273 -28
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-nrn6n2p1.js +0 -82
- package/dist/cli/guardrail-explain-9jmr8ezt.js +0 -83
- package/dist/tools/epic-plan-waves.d.ts +0 -79
- package/dist/tools/epic-record-divergence.d.ts +0 -73
- package/dist/tools/epic-run-phase.d.ts +0 -179
- package/dist/turbo/epic/activation.d.ts +0 -193
- package/dist/turbo/epic/calibration-engine.d.ts +0 -88
- package/dist/turbo/epic/calibration.d.ts +0 -80
- package/dist/turbo/epic/divergence-recorder.d.ts +0 -123
- package/dist/turbo/epic/index.d.ts +0 -24
- package/dist/turbo/epic/promotion-evidence.d.ts +0 -42
- package/dist/turbo/epic/state.d.ts +0 -69
- package/dist/turbo/epic/task-commit.d.ts +0 -110
- package/dist/turbo/epic/upstream-commits.d.ts +0 -82
- package/dist/turbo/epic/wave-planner.d.ts +0 -83
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic v2 C8 — the epic scorecard (pure over the epic record).
|
|
3
|
+
*
|
|
4
|
+
* One bounded summary of how an epic ran, computed from what the lifecycle
|
|
5
|
+
* record already holds (waves, per-task outcomes recorded at wave close,
|
|
6
|
+
* phase review runs) — no I/O here. The close report embeds it
|
|
7
|
+
* (`epic-report-v2`, `close.ts`) and `/swarm epic report` shows it live for
|
|
8
|
+
* the open epic and for past epics (`report.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Time (`wave-span-v1`):
|
|
11
|
+
* spanMs = Σ over CLOSED waves of (closedAt − issuedAt)
|
|
12
|
+
* workMs = Σ over tasks completed in a closed wave of
|
|
13
|
+
* (resolvedAt − issuedAt of that wave), each clamped ≥ 0
|
|
14
|
+
* concurrencyFactor = workMs / spanMs (null when spanMs = 0)
|
|
15
|
+
* It is a CONCURRENCY FACTOR, NOT A SPEEDUP (critic M12): a task's
|
|
16
|
+
* issue→resolve time includes queueing behind serialized QA (reviewers and
|
|
17
|
+
* test engineers run one task at a time), and a wave closes only when the
|
|
18
|
+
* architect next calls `epic_next_wave`. It says how much task time
|
|
19
|
+
* overlapped, never how much faster the epic was than a serial run.
|
|
20
|
+
* interWaveIdleMs = Σ max(0, issuedAt(next) − closedAt(previous)).
|
|
21
|
+
*
|
|
22
|
+
* Gate rates are first-pass rates over the tasks completed in the epic;
|
|
23
|
+
* the failure counts they come from are LOWER BOUNDS (the evidence
|
|
24
|
+
* workflow keeps only its last 3 retry entries; a phase keeps at most
|
|
25
|
+
* {@link EPIC_PHASE_VERDICTS_KEEP} review verdicts), so the rates are upper
|
|
26
|
+
* bounds — `boundedHistory: true` says so.
|
|
27
|
+
*/
|
|
28
|
+
import type { EpicCloseOutcome, EpicRecordV1 } from './lifecycle.js';
|
|
29
|
+
import { type EpicSizingVerdict } from './sizing.js';
|
|
30
|
+
export declare const EPIC_SCORECARD_SCHEMA = "epic-scorecard-v1";
|
|
31
|
+
/** Undeclared files listed (sorted); the total is kept separately. */
|
|
32
|
+
export declare const EPIC_SCORECARD_FILES_KEEP = 20;
|
|
33
|
+
/** Hot files listed (hottest first). */
|
|
34
|
+
export declare const EPIC_SCORECARD_HOT_FILES_KEEP = 10;
|
|
35
|
+
export declare const EPIC_CONCURRENCY_FACTOR_LABEL = "concurrency factor, not speedup; includes queueing behind serialized QA";
|
|
36
|
+
/** What the scorecard reads from an epic record (a v1 report has these too). */
|
|
37
|
+
export type EpicScorecardSource = Pick<EpicRecordV1, 'epicKey' | 'planId' | 'startedAt' | 'forced' | 'sizing' | 'priorDigest' | 'waves' | 'tasks' | 'phases'>;
|
|
38
|
+
export interface EpicScorecardRate {
|
|
39
|
+
passed: number;
|
|
40
|
+
of: number;
|
|
41
|
+
/** passed / of, 3 decimals; null when `of` is 0. */
|
|
42
|
+
rate: number | null;
|
|
43
|
+
}
|
|
44
|
+
export interface EpicScorecardV1 {
|
|
45
|
+
schema: typeof EPIC_SCORECARD_SCHEMA;
|
|
46
|
+
epicKey: string;
|
|
47
|
+
planId: string;
|
|
48
|
+
/** `open` for the live scorecard of the open epic. */
|
|
49
|
+
outcome: EpicCloseOutcome | 'open';
|
|
50
|
+
startedAt: string;
|
|
51
|
+
closedAt: string | null;
|
|
52
|
+
forced: boolean;
|
|
53
|
+
sizingAtStart: EpicSizingVerdict;
|
|
54
|
+
tasks: {
|
|
55
|
+
/** Tasks of the plan (null when the plan is gone or orphaned). */
|
|
56
|
+
total: number | null;
|
|
57
|
+
/** Tasks resolved `completed` through an epic wave. */
|
|
58
|
+
completedInEpic: number;
|
|
59
|
+
/** Tasks already completed when the epic started (when known). */
|
|
60
|
+
adoptedAtStart?: number;
|
|
61
|
+
/** Tasks issued alone in an `exclusive` wave (open wave included). */
|
|
62
|
+
exclusive: number;
|
|
63
|
+
/** Tasks issued alone as one task of a densely coupled component. */
|
|
64
|
+
serialComponent: number;
|
|
65
|
+
};
|
|
66
|
+
waves: {
|
|
67
|
+
/** Waves issued, the open one included (aborted waves excluded). */
|
|
68
|
+
count: number;
|
|
69
|
+
/** Waves with two or more tasks. */
|
|
70
|
+
parallel: number;
|
|
71
|
+
meanWidth: number;
|
|
72
|
+
maxWidth: number;
|
|
73
|
+
};
|
|
74
|
+
time: {
|
|
75
|
+
method: 'wave-span-v1';
|
|
76
|
+
label: typeof EPIC_CONCURRENCY_FACTOR_LABEL;
|
|
77
|
+
spanMs: number;
|
|
78
|
+
workMs: number;
|
|
79
|
+
/** workMs / spanMs, 3 decimals; null when no closed wave has a span. */
|
|
80
|
+
concurrencyFactor: number | null;
|
|
81
|
+
interWaveIdleMs: number;
|
|
82
|
+
};
|
|
83
|
+
conflicts: {
|
|
84
|
+
/** Tasks whose worktree merge-back failed while their wave ran. */
|
|
85
|
+
mergeFailures: number;
|
|
86
|
+
/** Tasks that wrote files outside their declared scope. */
|
|
87
|
+
undeclaredWriteTasks: number;
|
|
88
|
+
/** Undeclared files (task + wave level), sorted, first 20. */
|
|
89
|
+
undeclaredFiles: string[];
|
|
90
|
+
undeclaredFilesTotal: number;
|
|
91
|
+
};
|
|
92
|
+
rework: {
|
|
93
|
+
/** Tasks whose evidence workflow reached generation ≥ 2. */
|
|
94
|
+
tasksWithRework: number;
|
|
95
|
+
/** Σ max(0, generation − 1). */
|
|
96
|
+
extraGenerations: number;
|
|
97
|
+
/** Σ ledger transitions out of `completed` during the epic. */
|
|
98
|
+
reopened: number;
|
|
99
|
+
};
|
|
100
|
+
gates: {
|
|
101
|
+
stageAFirstPass: EpicScorecardRate;
|
|
102
|
+
stageBFirstPass: EpicScorecardRate;
|
|
103
|
+
/** Phases whose FIRST review run was approved by reviewer and critic. */
|
|
104
|
+
phaseReviewFirstPass: EpicScorecardRate;
|
|
105
|
+
boundedHistory: true;
|
|
106
|
+
};
|
|
107
|
+
learning: {
|
|
108
|
+
/** Digest of the project prior the epic inherited (null: neutral). */
|
|
109
|
+
priorDigest: string | null;
|
|
110
|
+
/** Hot files of the epic's learned state, hottest first (≤ 10). */
|
|
111
|
+
topHotFiles: string[];
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export interface EpicScorecardInput {
|
|
115
|
+
record: EpicScorecardSource;
|
|
116
|
+
outcome: EpicCloseOutcome | 'open';
|
|
117
|
+
closedAt: string | null;
|
|
118
|
+
/** Plan task count (null/absent: unknown). */
|
|
119
|
+
planTaskTotal?: number | null;
|
|
120
|
+
/** Tasks completed before the epic started (absent: unknown). */
|
|
121
|
+
adoptedAtStart?: number;
|
|
122
|
+
/** Hot files of the learned state (any order is kept; bounded here). */
|
|
123
|
+
hotFiles?: readonly string[];
|
|
124
|
+
}
|
|
125
|
+
/** The scorecard of `input.record` (pure; never throws on a sane record). */
|
|
126
|
+
export declare function computeEpicScorecard(input: EpicScorecardInput): EpicScorecardV1;
|
|
127
|
+
/** `1h 02m`, `3m 05s`, `12s`, `0s`. */
|
|
128
|
+
export declare function formatEpicDuration(durationMs: number): string;
|
|
129
|
+
/** Markdown lines for `/swarm epic report` (bounded by the scorecard). */
|
|
130
|
+
export declare function formatEpicScorecardLines(card: EpicScorecardV1): string[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic v2 C7 — THE plan sizing (pure), shared by `/swarm epic start` and
|
|
3
|
+
* plan shaping (`shaping.ts`), bounded by a work budget.
|
|
4
|
+
*
|
|
5
|
+
* T pending tasks (status ∉ {completed, closed}), C of them with an
|
|
6
|
+
* estimated scope (live declared scope, else `files_touched`), L = Σ over
|
|
7
|
+
* phases of the dry-run steps of the component planner
|
|
8
|
+
* (`dryRunEpicPhase`, `components.ts`: waves + tasks it can never
|
|
9
|
+
* schedule), then `evaluateEpicSizing` (`sizing.ts`: S = T / L, Amdahl
|
|
10
|
+
* S_eff). Cross-phase dependencies count as satisfied (phases run in order).
|
|
11
|
+
*
|
|
12
|
+
* Bounded: every dry-run step is charged its deterministic cost
|
|
13
|
+
* (`epicWaveWork`, ≈ path comparisons) against `maxWork`; when the budget
|
|
14
|
+
* runs out the remaining tasks count as one serial step each (a
|
|
15
|
+
* pessimistic L — never an optimistic one) and the sizing is `aborted`.
|
|
16
|
+
* Without `maxWork` the sizing is unbounded and exact.
|
|
17
|
+
*
|
|
18
|
+
* Pure except {@link isDirectoryOnDisk}, the one bounded stat callers pass
|
|
19
|
+
* to shaping for scope entries without an extension.
|
|
20
|
+
*/
|
|
21
|
+
import type { LeanTurboConfig, PluginConfig } from '../config/schema.js';
|
|
22
|
+
import type { PlanTask as PartitionTask } from '../turbo/lean/partition-common.js';
|
|
23
|
+
import type { EpicPlanningSignals } from './planning-signals.js';
|
|
24
|
+
import { type EpicSizingThresholds, type EpicSizingVerdict } from './sizing.js';
|
|
25
|
+
/**
|
|
26
|
+
* Work units `/swarm epic start` may spend sizing a plan (≈ 1.5 s on a
|
|
27
|
+
* laptop; plan shaping's own allowance is `MAX_SHAPING_WORK`). Beyond it
|
|
28
|
+
* the remaining tasks count as serial and the refusal says the plan was
|
|
29
|
+
* too large to size exactly.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MAX_START_SIZING_WORK = 60000000;
|
|
32
|
+
/**
|
|
33
|
+
* Is the project-relative scope `entry` a directory on disk? One `statSync`
|
|
34
|
+
* (false on any error or an escaping path).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isDirectoryOnDisk(directory: string, entry: string): boolean;
|
|
37
|
+
/** The plan view sizing and shaping read (a structural subset of `Plan`). */
|
|
38
|
+
export interface EpicShapingTask {
|
|
39
|
+
id: string;
|
|
40
|
+
status?: string;
|
|
41
|
+
size?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
depends?: readonly string[];
|
|
44
|
+
files_touched?: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
export interface EpicShapingPhase {
|
|
47
|
+
id: number;
|
|
48
|
+
tasks?: readonly EpicShapingTask[];
|
|
49
|
+
}
|
|
50
|
+
/** Pending = still to run (the sizing's T): not completed and not closed. */
|
|
51
|
+
export declare function isEpicPendingStatus(status: string | undefined): boolean;
|
|
52
|
+
export interface EpicScopeEstimate {
|
|
53
|
+
/** Pending task id → estimated scope. */
|
|
54
|
+
scopes: Record<string, string[]>;
|
|
55
|
+
pendingIds: string[];
|
|
56
|
+
/** Pending tasks with a non-empty estimated scope. */
|
|
57
|
+
scoped: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Estimated scope of every pending task: the live declared scope, else
|
|
61
|
+
* `files_touched` (declarations are per phase and expire, so most tasks
|
|
62
|
+
* are estimated from the plan).
|
|
63
|
+
*/
|
|
64
|
+
export declare function estimateEpicScopes(phases: readonly EpicShapingPhase[], declared: Readonly<Record<string, readonly string[]>>): EpicScopeEstimate;
|
|
65
|
+
/** Everything the dry run needs besides the tasks and their scopes. */
|
|
66
|
+
export interface EpicPlanSizingContext {
|
|
67
|
+
directory: string;
|
|
68
|
+
leanConfig: LeanTurboConfig;
|
|
69
|
+
signals: EpicPlanningSignals;
|
|
70
|
+
maxParallel: number;
|
|
71
|
+
thresholds: EpicSizingThresholds;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The epic's wave width: `turbo.lean.max_parallel_coders` (default 4) in a
|
|
75
|
+
* git project; 1 otherwise (a non-git epic runs serially).
|
|
76
|
+
*/
|
|
77
|
+
export declare function epicWaveWidth(config: Pick<PluginConfig, 'turbo'>, isGitRepo: boolean): number;
|
|
78
|
+
/** The sizing context of `config` (lean risk policy + sizing thresholds). */
|
|
79
|
+
export declare function epicSizingContextFor(directory: string, config: Pick<PluginConfig, 'turbo' | 'epic'>, maxParallel: number, signals: EpicPlanningSignals): EpicPlanSizingContext;
|
|
80
|
+
/** One phase's dry run as the sizing counts it. */
|
|
81
|
+
export interface EpicPhaseRun {
|
|
82
|
+
/** Serial steps: waves + unscheduled tasks. */
|
|
83
|
+
steps: number;
|
|
84
|
+
/** Tasks the run could not schedule (cycle, blocked, or budget). */
|
|
85
|
+
unscheduled: number;
|
|
86
|
+
/** Work units spent. */
|
|
87
|
+
work: number;
|
|
88
|
+
/** The run stopped at its work budget. */
|
|
89
|
+
aborted: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** The pending tasks of a phase, as the planner reads them. */
|
|
92
|
+
export declare function pendingTasksOf(phase: EpicShapingPhase): PartitionTask[];
|
|
93
|
+
/** Dry-run one phase's `tasks` (bounded by `maxWork` when given). */
|
|
94
|
+
export declare function runEpicPhase(context: EpicPlanSizingContext, tasks: readonly PartitionTask[], scopes: Record<string, string[]>, maxWork?: number): EpicPhaseRun;
|
|
95
|
+
export interface EpicPlanSizing {
|
|
96
|
+
verdict: EpicSizingVerdict;
|
|
97
|
+
/** Phase id → its dry run (phases with pending tasks only). */
|
|
98
|
+
phases: Map<number, EpicPhaseRun>;
|
|
99
|
+
/** Work units spent over all phases. */
|
|
100
|
+
work: number;
|
|
101
|
+
/** A phase's dry run stopped at the budget: L is pessimistic. */
|
|
102
|
+
aborted: boolean;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* THE sizing of a plan (see the module header). `maxWork` bounds the total
|
|
106
|
+
* work over every phase.
|
|
107
|
+
*/
|
|
108
|
+
export declare function sizeEpicPlan(context: EpicPlanSizingContext, phases: readonly EpicShapingPhase[], estimate: EpicScopeEstimate, maxWork?: number): EpicPlanSizing;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic v2 C7 — plan-shaping suggestion builders (pure; orchestrated by
|
|
3
|
+
* `shapeEpicPlan` in `shaping.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Every suggestion that changes the plan is a CONCRETE `save_plan` patch
|
|
6
|
+
* (complete resulting `files_touched` / `depends` per touched task, new
|
|
7
|
+
* task ids unused and valid) and is judged by a what-if: the patch applied
|
|
8
|
+
* to the phase, the phase dry-run again (`runEpicPhase`), L and S_eff
|
|
9
|
+
* recomputed. A what-if is rejected when it leaves more tasks unschedulable
|
|
10
|
+
* than the baseline (a dependency cycle the patch would create), when it
|
|
11
|
+
* does not finish within the work budget, or when it does not help.
|
|
12
|
+
*
|
|
13
|
+
* Work is metered ({@link EpicWorkMeter}): graph builds, edge-driver
|
|
14
|
+
* lookups, merge pair scans and every dry run are charged their
|
|
15
|
+
* deterministic cost; when the meter runs out the remaining candidates are
|
|
16
|
+
* skipped (`truncated`).
|
|
17
|
+
*/
|
|
18
|
+
import type { PlanTask as PartitionTask } from '../turbo/lean/partition-common.js';
|
|
19
|
+
import { type EpicConflictGraph } from './components.js';
|
|
20
|
+
import { type EpicPlanSizing, type EpicPlanSizingContext, type EpicScopeEstimate, type EpicShapingPhase } from './shaping-sizing.js';
|
|
21
|
+
/** Hub rule: a file declared by at least this many tasks… */
|
|
22
|
+
export declare const HUB_MIN_TASKS = 3;
|
|
23
|
+
/** …driving at least this share of the plan's conflict edges… */
|
|
24
|
+
export declare const HUB_MIN_EDGE_SHARE = 0.25;
|
|
25
|
+
/** …or whose extraction gains at least this much S_eff. */
|
|
26
|
+
export declare const HUB_MIN_DELTA = 0.25;
|
|
27
|
+
/** Merge rule: scope Jaccard similarity at least this. */
|
|
28
|
+
export declare const MERGE_MIN_JACCARD = 0.8;
|
|
29
|
+
/** Deterministic work meter (units ≈ path comparisons, `epicWaveWork`). */
|
|
30
|
+
export declare class EpicWorkMeter {
|
|
31
|
+
readonly max: number;
|
|
32
|
+
spent: number;
|
|
33
|
+
truncated: boolean;
|
|
34
|
+
constructor(max: number);
|
|
35
|
+
get left(): number;
|
|
36
|
+
/** Spend `cost` if it fits; otherwise mark the meter truncated. */
|
|
37
|
+
charge(cost: number): boolean;
|
|
38
|
+
}
|
|
39
|
+
/** What a suggestion would do to the sizing (the changed plan's sizing). */
|
|
40
|
+
export interface EpicShapingWhatIf {
|
|
41
|
+
effectiveSpeedup: number;
|
|
42
|
+
serialSteps: number;
|
|
43
|
+
pendingTasks: number;
|
|
44
|
+
epicSized: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** One task edit of a patch; the resulting fields are complete. */
|
|
47
|
+
export interface EpicShapingTaskEdit {
|
|
48
|
+
taskId: string;
|
|
49
|
+
/** The task's complete `depends` after the edit. */
|
|
50
|
+
depends: string[];
|
|
51
|
+
/** The task's complete `files_touched` after the edit (when changed). */
|
|
52
|
+
files_touched?: string[];
|
|
53
|
+
/** Delta view of the same edit (extract / isolate). */
|
|
54
|
+
remove_files?: string[];
|
|
55
|
+
add_depends?: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface EpicShapingNewTask {
|
|
58
|
+
id: string;
|
|
59
|
+
phase: number;
|
|
60
|
+
description: string;
|
|
61
|
+
files_touched: string[];
|
|
62
|
+
depends: string[];
|
|
63
|
+
}
|
|
64
|
+
/** extract-prerequisite / isolate-hot-file: add `newTask`, apply `edits`. */
|
|
65
|
+
export interface EpicShapingPatch {
|
|
66
|
+
newTask: EpicShapingNewTask;
|
|
67
|
+
edits: EpicShapingTaskEdit[];
|
|
68
|
+
}
|
|
69
|
+
interface SuggestionBase {
|
|
70
|
+
summary: string;
|
|
71
|
+
/** ΔS_eff of the what-if (null: not computable — scope advice). */
|
|
72
|
+
deltaEffectiveSpeedup: number | null;
|
|
73
|
+
whatIf: EpicShapingWhatIf | null;
|
|
74
|
+
}
|
|
75
|
+
export type EpicShapingSuggestion = (SuggestionBase & {
|
|
76
|
+
type: 'declare-scope';
|
|
77
|
+
taskIds: string[];
|
|
78
|
+
count: number;
|
|
79
|
+
}) | (SuggestionBase & {
|
|
80
|
+
type: 'narrow-scope';
|
|
81
|
+
phase: number;
|
|
82
|
+
/** The directory entry that conflicts with every task under it. */
|
|
83
|
+
entry: string;
|
|
84
|
+
taskIds: string[];
|
|
85
|
+
}) | (SuggestionBase & {
|
|
86
|
+
type: 'extract-prerequisite' | 'isolate-hot-file';
|
|
87
|
+
phase: number;
|
|
88
|
+
file: string;
|
|
89
|
+
fileKind: 'global-file' | 'hot-file' | 'hub-file';
|
|
90
|
+
taskIds: string[];
|
|
91
|
+
edgeShare: number;
|
|
92
|
+
patch: EpicShapingPatch;
|
|
93
|
+
}) | (SuggestionBase & {
|
|
94
|
+
type: 'split-task';
|
|
95
|
+
phase: number;
|
|
96
|
+
taskId: string;
|
|
97
|
+
/** One file set per part (part 1 keeps the task id). */
|
|
98
|
+
parts: string[][];
|
|
99
|
+
patch: {
|
|
100
|
+
newTasks: EpicShapingNewTask[];
|
|
101
|
+
edits: EpicShapingTaskEdit[];
|
|
102
|
+
};
|
|
103
|
+
}) | (SuggestionBase & {
|
|
104
|
+
type: 'merge-tasks';
|
|
105
|
+
phase: number;
|
|
106
|
+
keep: string;
|
|
107
|
+
absorb: string;
|
|
108
|
+
jaccard: number;
|
|
109
|
+
patch: {
|
|
110
|
+
removed_task_ids: string[];
|
|
111
|
+
removal_reason: string;
|
|
112
|
+
edits: EpicShapingTaskEdit[];
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
/** Everything a builder needs about the baseline. */
|
|
116
|
+
export interface ShapingBase {
|
|
117
|
+
context: EpicPlanSizingContext;
|
|
118
|
+
phases: readonly EpicShapingPhase[];
|
|
119
|
+
estimate: EpicScopeEstimate;
|
|
120
|
+
sizing: EpicPlanSizing;
|
|
121
|
+
hot: ReadonlySet<string>;
|
|
122
|
+
/** Every task id of the plan, any status (new ids must be unused). */
|
|
123
|
+
usedIds: ReadonlySet<string>;
|
|
124
|
+
/** Task id → depends, every task of the plan. */
|
|
125
|
+
dependsOf: ReadonlyMap<string, readonly string[]>;
|
|
126
|
+
/** Task id → the tasks that depend on it, every task of the plan. */
|
|
127
|
+
dependents: ReadonlyMap<string, readonly string[]>;
|
|
128
|
+
/** Optional on-disk directory check for scope entries (no-extension names). */
|
|
129
|
+
isDirectory?: (entry: string) => boolean;
|
|
130
|
+
meter: EpicWorkMeter;
|
|
131
|
+
}
|
|
132
|
+
/** Next unused `<phase>.<n>` task id (and reserve it in `taken`). */
|
|
133
|
+
export declare function nextTaskId(phaseId: number, taken: Set<string>): string;
|
|
134
|
+
/** Does `from` (transitively, through `depends`) depend on `target`? */
|
|
135
|
+
export declare function dependsOnTransitively(base: ShapingBase, from: string, target: string): boolean;
|
|
136
|
+
export interface PhaseModel {
|
|
137
|
+
phase: EpicShapingPhase;
|
|
138
|
+
tasks: PartitionTask[];
|
|
139
|
+
graph: EpicConflictGraph;
|
|
140
|
+
edgeCount: number;
|
|
141
|
+
/** File → edges it drives (path overlaps; the shorter path drives). */
|
|
142
|
+
driven: Map<string, number>;
|
|
143
|
+
/** File → tasks it makes exclusive (global / hot). */
|
|
144
|
+
exclusiveCaused: Map<string, number>;
|
|
145
|
+
}
|
|
146
|
+
/** Model one phase, or null (no pending task, or out of budget). */
|
|
147
|
+
export declare function modelPhase(base: ShapingBase, phase: EpicShapingPhase): PhaseModel | null;
|
|
148
|
+
/**
|
|
149
|
+
* A scope entry that names a directory: a trailing separator, a prefix of
|
|
150
|
+
* another scope path of the plan, or (no extension) a directory on disk
|
|
151
|
+
* when the caller can check — without a checker a name without an
|
|
152
|
+
* extension counts as a directory.
|
|
153
|
+
*/
|
|
154
|
+
export declare function isDirectoryEntry(base: ShapingBase, entry: string, rawEntries: readonly string[], allPaths: ReadonlySet<string>): boolean;
|
|
155
|
+
export declare function narrowScopeSuggestion(phase: EpicShapingPhase, entry: string, taskIds: string[]): EpicShapingSuggestion;
|
|
156
|
+
export declare function fileSuggestion(base: ShapingBase, model: PhaseModel, file: string, totalEdges: number): EpicShapingSuggestion | null;
|
|
157
|
+
/** Articulation points of the non-exclusive conflict graph (Tarjan). */
|
|
158
|
+
export declare function articulationPoints(graph: EpicConflictGraph): string[];
|
|
159
|
+
export declare function splitSuggestion(base: ShapingBase, model: PhaseModel, taskId: string): EpicShapingSuggestion | null;
|
|
160
|
+
export declare function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number;
|
|
161
|
+
/**
|
|
162
|
+
* Merge `absorb` into `keep`. Skipped when one reaches the other through
|
|
163
|
+
* another task (the merged task would depend on itself); a direct
|
|
164
|
+
* dependency between them is fine (it disappears).
|
|
165
|
+
*/
|
|
166
|
+
export declare function mergeSuggestion(base: ShapingBase, phase: EpicShapingPhase, keep: PartitionTask, absorb: PartitionTask, similarity: number): EpicShapingSuggestion | null;
|
|
167
|
+
/** Every task of the plan: id → depends, and id → dependents. */
|
|
168
|
+
export declare function dependencyIndex(phases: readonly EpicShapingPhase[]): {
|
|
169
|
+
dependsOf: Map<string, readonly string[]>;
|
|
170
|
+
dependents: Map<string, string[]>;
|
|
171
|
+
};
|
|
172
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic v2 C7 — plan shaping (pure).
|
|
3
|
+
*
|
|
4
|
+
* Tells the architect HOW a plan could run better as an epic, with the same
|
|
5
|
+
* model `/swarm epic start` sizes it with (`shaping-sizing.ts`: a dry run
|
|
6
|
+
* of the component planner, `components.ts`, then `sizing.ts`):
|
|
7
|
+
*
|
|
8
|
+
* 1. baseline: the plan's sizing (T, C, L, S_eff) — passed in by
|
|
9
|
+
* `/swarm epic start` (its own sizing), computed otherwise;
|
|
10
|
+
* 2. the conflict graph of each phase and the files DRIVING its edges (a
|
|
11
|
+
* path overlap between two expanded scopes is driven by the shorter
|
|
12
|
+
* path); edge share = edges a file drives / all edges of the plan;
|
|
13
|
+
* global/hot files also count the tasks they make exclusive;
|
|
14
|
+
* 3. what-ifs (`shaping-suggestions.ts`): each suggestion is a concrete
|
|
15
|
+
* save_plan patch, applied to its phase and dry-run again → ΔS_eff;
|
|
16
|
+
* a patch that would leave more tasks unschedulable (a cycle) is
|
|
17
|
+
* rejected;
|
|
18
|
+
* 4. suggestions, capped at {@link MAX_SHAPING_SUGGESTIONS}: scope advice
|
|
19
|
+
* first — `declare-scope` (tasks with no scope run alone) and
|
|
20
|
+
* `narrow-scope` (a directory entry driving conflicts: it cannot be
|
|
21
|
+
* extracted) — then by ΔS_eff: `extract-prerequisite` (a hub file of
|
|
22
|
+
* ≥ 3 tasks with edge share ≥ 0.25 or ΔS_eff ≥ 0.25 moved into a new
|
|
23
|
+
* prerequisite task), `isolate-hot-file` (a learned hot file of fewer
|
|
24
|
+
* tasks, same patch), `split-task` (an articulation task, one part per
|
|
25
|
+
* cluster), `merge-tasks` (two small tasks with scope Jaccard ≥ 0.8;
|
|
26
|
+
* offered at ΔS_eff ≥ 0 — they serialize anyway and one task saves a
|
|
27
|
+
* QA cycle).
|
|
28
|
+
*
|
|
29
|
+
* Verdicts: `acceptable` (epic-sized; nothing worth changing),
|
|
30
|
+
* `improvable` (a suggestion makes the plan epic-sized, improves an
|
|
31
|
+
* epic-sized plan by ≥ {@link MIN_USEFUL_DELTA}, or scope advice applies),
|
|
32
|
+
* `not-epic-sized` (no suggestion makes it epic-sized — run it in Balanced),
|
|
33
|
+
* `skipped-budget` (over {@link MAX_SHAPING_TASKS} pending tasks or
|
|
34
|
+
* {@link MAX_SHAPING_FILES} distinct scope files, or the baseline did not
|
|
35
|
+
* fit the work budget, or no what-if did).
|
|
36
|
+
*
|
|
37
|
+
* Bounded by WORK, not by a clock: one {@link MAX_SHAPING_WORK} meter
|
|
38
|
+
* (deterministic units ≈ path comparisons, `epicWaveWork`) is charged for
|
|
39
|
+
* the baseline dry runs (each step before it runs), the graph builds, the
|
|
40
|
+
* edge-driver lookups, the merge pair scan and every what-if dry run; when
|
|
41
|
+
* it runs out the rest is skipped. Calibrated so the worst plan within the
|
|
42
|
+
* caps adds ≈ 0.3 s to a save_plan on a laptop. A synchronous computation
|
|
43
|
+
* cannot be interrupted by a timeout (`withTimeout` would only stop
|
|
44
|
+
* waiting), so there is no timeout verdict — the budget is the bound.
|
|
45
|
+
*
|
|
46
|
+
* Analysis only: nothing here is write authorization or plan state.
|
|
47
|
+
*/
|
|
48
|
+
import { type EpicPlanSizing, type EpicPlanSizingContext, type EpicShapingPhase } from './shaping-sizing.js';
|
|
49
|
+
import { type EpicShapingSuggestion } from './shaping-suggestions.js';
|
|
50
|
+
import { type EpicSizingVerdict } from './sizing.js';
|
|
51
|
+
/** Plans with more pending tasks are not shaped (`skipped-budget`). */
|
|
52
|
+
export declare const MAX_SHAPING_TASKS = 200;
|
|
53
|
+
/** Plans whose pending scopes name more distinct files are not shaped. */
|
|
54
|
+
export declare const MAX_SHAPING_FILES = 500;
|
|
55
|
+
/** File what-ifs (extract / isolate), top candidates only. */
|
|
56
|
+
export declare const MAX_WHAT_IF_FILES = 10;
|
|
57
|
+
/** Split-task what-ifs (articulation tasks, highest degree first). */
|
|
58
|
+
export declare const MAX_WHAT_IF_SPLITS = 3;
|
|
59
|
+
/** Merge-tasks what-ifs (highest Jaccard first). */
|
|
60
|
+
export declare const MAX_WHAT_IF_MERGES = 3;
|
|
61
|
+
/**
|
|
62
|
+
* Work units for one whole shaping (baseline + analysis + what-ifs) — the
|
|
63
|
+
* save_plan latency bound (see the module header).
|
|
64
|
+
*/
|
|
65
|
+
export declare const MAX_SHAPING_WORK = 6000000;
|
|
66
|
+
/** Suggestions returned. */
|
|
67
|
+
export declare const MAX_SHAPING_SUGGESTIONS = 5;
|
|
68
|
+
/** An epic-sized plan is `improvable` only for a gain at least this. */
|
|
69
|
+
export declare const MIN_USEFUL_DELTA = 0.05;
|
|
70
|
+
export type EpicShapingVerdictKind = 'acceptable' | 'improvable' | 'not-epic-sized' | 'skipped-budget';
|
|
71
|
+
export interface EpicShapingReport {
|
|
72
|
+
verdict: EpicShapingVerdictKind;
|
|
73
|
+
/** Baseline sizing (null when it was not (fully) computed). */
|
|
74
|
+
sizing: EpicSizingVerdict | null;
|
|
75
|
+
suggestions: EpicShapingSuggestion[];
|
|
76
|
+
budget: {
|
|
77
|
+
tasks: number;
|
|
78
|
+
files: number;
|
|
79
|
+
maxTasks: number;
|
|
80
|
+
maxFiles: number;
|
|
81
|
+
whatIfs: number;
|
|
82
|
+
/** Work units spent and the allowance. */
|
|
83
|
+
work: number;
|
|
84
|
+
maxWork: number;
|
|
85
|
+
/** Something was skipped because the work budget ran out. */
|
|
86
|
+
truncated: boolean;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export interface EpicShapingInput extends EpicPlanSizingContext {
|
|
90
|
+
phases: readonly EpicShapingPhase[];
|
|
91
|
+
/** Live declared scope per task id (`[]`/absent ⇒ `files_touched`). */
|
|
92
|
+
declared: Readonly<Record<string, readonly string[]>>;
|
|
93
|
+
/**
|
|
94
|
+
* The caller's own sizing of this plan with these inputs (the start's) —
|
|
95
|
+
* reused instead of a second baseline. An aborted one ⇒ skipped-budget.
|
|
96
|
+
*/
|
|
97
|
+
baseline?: EpicPlanSizing;
|
|
98
|
+
/** On-disk directory check for no-extension scope entries (optional). */
|
|
99
|
+
isDirectory?: (entry: string) => boolean;
|
|
100
|
+
/** Work allowance (default {@link MAX_SHAPING_WORK}). */
|
|
101
|
+
maxWork?: number;
|
|
102
|
+
}
|
|
103
|
+
/** Shape one plan (see the module header). */
|
|
104
|
+
export declare function shapeEpicPlan(input: EpicShapingInput): EpicShapingReport;
|
|
105
|
+
/** Why shaping was skipped (`skipped-budget`). */
|
|
106
|
+
export declare function describeEpicShapingSkip(report: EpicShapingReport): string;
|
|
107
|
+
/** `<reason>; <reason>` — the sizing reasons as one line. */
|
|
108
|
+
export declare function describeEpicShapingReasons(sizing: EpicSizingVerdict): string;
|
|
109
|
+
/** Markdown lines of the advisory (`/swarm coupling --suggest`, start). */
|
|
110
|
+
export declare function formatEpicShapingLines(report: EpicShapingReport, options?: {
|
|
111
|
+
maxSuggestions?: number;
|
|
112
|
+
}): string[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic sizing (Epic v2 C1a) — pure.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a plan is worth running as an epic. Inputs are counted by
|
|
5
|
+
* the caller (`src/epic/start.ts`):
|
|
6
|
+
* T = pending tasks (status ∉ {completed, closed}) across the plan
|
|
7
|
+
* C = pending tasks with a non-empty scope (live declared scope, else
|
|
8
|
+
* `files_touched`)
|
|
9
|
+
* L = serial steps of a dry-run of the Epic component planner
|
|
10
|
+
* (`components.ts`) over every phase with pending tasks (waves + tasks
|
|
11
|
+
* it can never schedule), under the epic's own wave-width cap
|
|
12
|
+
*
|
|
13
|
+
* coverage = C / T, S = T / L, S_eff = 1 / ((1 − c) + c / S) (Amdahl)
|
|
14
|
+
*
|
|
15
|
+
* where c = `epic.sizing.coder_fraction` is the share of a task's
|
|
16
|
+
* wall-clock that parallel coders actually overlap (Stage A/B QA and the
|
|
17
|
+
* architect's turns stay serial). Epic-sized ⇔ T ≥ min_tasks ∧
|
|
18
|
+
* coverage ≥ min_scope_coverage ∧ S_eff ≥ min_effective_speedup.
|
|
19
|
+
*/
|
|
20
|
+
/** Resolved thresholds (config keys `epic.sizing.*`). */
|
|
21
|
+
export interface EpicSizingThresholds {
|
|
22
|
+
minTasks: number;
|
|
23
|
+
minScopeCoverage: number;
|
|
24
|
+
minEffectiveSpeedup: number;
|
|
25
|
+
coderFraction: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const DEFAULT_EPIC_SIZING_THRESHOLDS: EpicSizingThresholds;
|
|
28
|
+
export type EpicSizingReason = 'too-few-tasks' | 'insufficient-scope-coverage' | 'insufficient-parallelism';
|
|
29
|
+
export interface EpicSizingInput {
|
|
30
|
+
pendingTasks: number;
|
|
31
|
+
scopedTasks: number;
|
|
32
|
+
serialSteps: number;
|
|
33
|
+
}
|
|
34
|
+
export interface EpicSizingVerdict {
|
|
35
|
+
epicSized: boolean;
|
|
36
|
+
reasons: EpicSizingReason[];
|
|
37
|
+
pendingTasks: number;
|
|
38
|
+
scopedTasks: number;
|
|
39
|
+
scopeCoverage: number;
|
|
40
|
+
serialSteps: number;
|
|
41
|
+
/** Raw concurrency S = T / L (1 when L = 0). */
|
|
42
|
+
concurrency: number;
|
|
43
|
+
/** Amdahl-adjusted speedup S_eff. */
|
|
44
|
+
effectiveSpeedup: number;
|
|
45
|
+
thresholds: EpicSizingThresholds;
|
|
46
|
+
}
|
|
47
|
+
/** S_eff = 1 / ((1 − c) + c / S). S ≤ 1 or c ≤ 0 ⇒ 1. */
|
|
48
|
+
export declare function computeEffectiveSpeedup(concurrency: number, coderFraction: number): number;
|
|
49
|
+
/** Config view consumed by {@link resolveEpicSizingThresholds}. */
|
|
50
|
+
export interface EpicSizingConfigView {
|
|
51
|
+
min_tasks?: number;
|
|
52
|
+
min_scope_coverage?: number;
|
|
53
|
+
min_effective_speedup?: number;
|
|
54
|
+
coder_fraction?: number;
|
|
55
|
+
}
|
|
56
|
+
export declare function resolveEpicSizingThresholds(sizing: EpicSizingConfigView | undefined): EpicSizingThresholds;
|
|
57
|
+
export declare function evaluateEpicSizing(input: EpicSizingInput, thresholds?: EpicSizingThresholds): EpicSizingVerdict;
|
|
58
|
+
export declare function describeEpicSizingReason(reason: EpicSizingReason, verdict: EpicSizingVerdict): string;
|
|
59
|
+
/** One-line summary for start/status/report output. */
|
|
60
|
+
export declare function summarizeEpicSizing(verdict: EpicSizingVerdict): string;
|