Implement Development Effort Calculator (WP-0010-T02)
src/target_revenue/effort_calculator.py implements Candidate A (labor-cost-anchored, accepted T01): commit-timestamp session-gap clustering for human interaction time, workplan/task-volume counts via direct workplans/ directory parsing (no state-hub dependency, works uniformly on any repo using this repo's own convention), file/line counts with generated/vendored-path exclusion, and caller-supplied token-cost pricing. estimate_target_basis() combines these and returns a derivation dict (every input shown) plus a warnings list - never a black-box dollar figure. 1-day manual-work floor, as requested: any raw commit-clustered estimate below 1.0 day is floored and flagged with a warning that this is very likely a measurement gap (commit-clustering is a floor estimate by design) that should usually be compensated for by manual override, not trusted at face value. A second, independent sanity-check warning fires when finished-workplan/task volume is substantial but the time estimate is still low, even above the floor - demonstrated live against target-revenue's own history (7 finished workplans, 57 tasks correctly flagged a 2.38-day estimate as under-counted). scripts/effort_calculator_cli.py: CLI wrapper printing JSON, following the same offline-first, no-Phase-declaration pattern as scripts/trf_onboard.py. tests/test_effort_calculator.py (15 deterministic tests, throwaway git repos/tmp_path fixtures) covers commit clustering, workplan/task parsing, size-metric exclusion, token-cost pricing, the floor-and-warning behavior, the sanity-check warning, and an end-to-end smoke test. No new hard dependency.
This commit is contained in:
parent
37ccee22ab
commit
9566e16a97
6 changed files with 720 additions and 2 deletions
57
scripts/effort_calculator_cli.py
Normal file
57
scripts/effort_calculator_cli.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python3
|
||||
"""CLI wrapper for target_revenue.effort_calculator (WP-0010-T02).
|
||||
|
||||
Prints a proposed `phase.target_basis` estimate for a repo, with full
|
||||
derivation and warnings, as JSON. Token counts are caller-supplied (e.g.
|
||||
pulled from the state hub's `get_token_summary` beforehand) rather than
|
||||
fetched here — this script has no MCP client of its own, matching the
|
||||
offline-first design of the rest of this library.
|
||||
|
||||
Does not select a Target Multiple and does not register anything with
|
||||
the hosted Trust Service — this is an estimation aid, not a Phase
|
||||
declaration tool (that remains `scripts/trf_onboard.py`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
|
||||
from target_revenue import effort_calculator as ec # noqa: E402
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("repo_path", help="path to the repo to estimate effort for")
|
||||
parser.add_argument("--daily-rate", type=float, required=True)
|
||||
parser.add_argument("--tokens-in", type=int, default=0)
|
||||
parser.add_argument("--tokens-out", type=int, default=0)
|
||||
parser.add_argument("--model", default="default")
|
||||
parser.add_argument("--session-gap-hours", type=float, default=ec.DEFAULT_SESSION_GAP_HOURS)
|
||||
parser.add_argument("--hours-per-day", type=float, default=ec.DEFAULT_HOURS_PER_DAY)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
result = ec.calculate_target_basis(
|
||||
args.repo_path,
|
||||
daily_rate=args.daily_rate,
|
||||
tokens_in=args.tokens_in,
|
||||
tokens_out=args.tokens_out,
|
||||
model=args.model,
|
||||
session_gap_hours=args.session_gap_hours,
|
||||
hours_per_day=args.hours_per_day,
|
||||
)
|
||||
|
||||
output = dataclasses.asdict(result)
|
||||
print(json.dumps(output, indent=2))
|
||||
if result.warnings:
|
||||
print(f"\n{len(result.warnings)} warning(s) — see 'warnings' above.", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue