Apply Development Effort Calculator to real pilot candidates (WP-0010-T03)
Adds since/until date-range scoping to cluster_commit_hours() and workplan_task_counts() (threaded through calculate_target_basis()), needed whenever a candidate is one bounded workplan within a repo whose overall history spans much more (net-kingdom, railiance-apps) rather than the whole repo being the candidate (vergabe-teilnahme, info-tech-canon). Fixes a real bug found along the way: workplan_task_counts() only scanned the top level of workplans/, missing net-kingdom's workplans/archived/ convention entirely - silently reported zero finished workplans for NK-WP-0002, which lives there. Fixed to scan recursively; added a regression test. Updates all three draft pilot-candidate manifests with calculator- derived target_basis/initial_target values, replacing the hand-picked placeholders: net-kingdom-local-identity: 200,000 -> 10,000 EUR (floor + sanity warnings) railiance-vergabe-teilnahme: 3,500,000 -> 648,800 EUR (no warnings) info-tech-canon-service-surface: 2,500,000 -> 141,800 EUR (sanity warning) history/260730-EffortCalculator-CandidateApplication.md records full derivation, warnings, and the judgment calls made explicit rather than silently picked (date-scoping windows; measuring vergabe-teilnahme's own repo rather than railiance-apps' deployment-only wiring, with both figures shown). Still draft/non-binding - WP-0008-T05 unaffected. 5 new tests (20 -> now covering since/until scoping and the archived-subdirectory fix). Full suite: 84 passing offline.
This commit is contained in:
parent
78a5e72bbf
commit
6eec4f6634
9 changed files with 405 additions and 25 deletions
|
|
@ -82,7 +82,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule**
|
|||
| [TREV-WP-0007](workplans/TREV-WP-0007-degeneration-policy-and-canonical-profiles.md) | Degeneration policy + canonical monetization profile catalog — **finished**, all 4 tasks done. `trsl:policy:linear-longstop-v0` confirmed 2026-07-29 as the v1 norm for the first pilot cohort; `progress-paused-longstop-v1` named as the next iteration, not yet adopted |
|
||||
| [TREV-WP-0008](workplans/TREV-WP-0008-governance-and-pilot-rollout.md) | Governance formalization + pilot rollout — active; T01–T04 done. `info-tech-canon` dry-run onboarding routine exercised end-to-end 2026-07-29. **Org-wide TRSL license adoption executed 2026-07-30** across ~90 `coulomb`-org repos (`history/260730-TRSL-OrgWideLicenseRollout.md`) — a license-text adoption, not a Phase declaration. T05 (real Phase go-live gate) remains `todo` by design; no Phase exists yet for any repo |
|
||||
| [TREV-WP-0009](workplans/TREV-WP-0009-target-revenue-control-plane.md) | Target Revenue Control Plane — interactive UI for the `binky` tenant, incl. interactive Development Credit entry creation (`specs/TargetRevenueControlPlaneConcept.md`) — active; **T01 accepted 2026-07-30** (four rights tiers confirmed; per-human sub-credentials at the Trust Service layer chosen over the concept's own simpler recommendation) — adds a real prerequisite: T02 extends WP-0006's finished auth layer before T03 (Control Plane backend) and T04 (interactive UI) can proceed |
|
||||
| [TREV-WP-0010](workplans/TREV-WP-0010-development-effort-calculator.md) | Development Effort Calculator — turns human time/workplan-task volume/repo size/AI token cost into `target_basis` values feeding the framework's existing Initial Target formula (`specs/DevelopmentEffortCalculatorConcept.md`) — active; T01 (Candidate A) and **T02 (implementation, `src/target_revenue/effort_calculator.py`, incl. a 1-day manual-work floor with warning) done**; T03 (apply to real candidates) next |
|
||||
| [TREV-WP-0010](workplans/TREV-WP-0010-development-effort-calculator.md) | Development Effort Calculator — **finished**, all 3 tasks done. Applied to the three real pilot candidates (`history/260730-EffortCalculator-CandidateApplication.md`) — every calculator-derived Initial Target came out materially lower than the earlier hand-picked placeholders, two of three carrying explicit warnings recommending manual review |
|
||||
|
||||
Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,14 @@ of how complete or plausible these examples look — including
|
|||
| `railiance-vergabe-teilnahme/` | `railiance-*` (`railiance-apps`) | `vergabe-teilnahme` (`RAILIANCE-WP-0002`/`-0014`) | Product-defining (100x) |
|
||||
| `info-tech-canon-service-surface/` | `info-tech-canon` | Cumulative service surface, `ITC-WP-0001`–`ITC-WP-0012` | Product-defining (100x) |
|
||||
|
||||
All figures (`initial_target`, `target_basis`) are illustrative
|
||||
placeholders for this worked-example exercise, not a real costing or
|
||||
pricing proposal — see `specs/PilotPhaseCandidateSurvey.md` for the
|
||||
underlying candidate rationale.
|
||||
**`target_basis`/`initial_target` figures (updated 2026-07-30):** all
|
||||
three now use `target_revenue.effort_calculator`-derived values
|
||||
(`workplans/TREV-WP-0010-development-effort-calculator.md` T03), not the
|
||||
originally hand-picked illustrative placeholders. Full derivation,
|
||||
warnings, and the judgment calls involved (date-scoping, and which of two
|
||||
repos to measure for `railiance-vergabe-teilnahme`) are in
|
||||
`history/260730-EffortCalculator-CandidateApplication.md`. Two of the
|
||||
three carry explicit calculator warnings recommending a manual override
|
||||
before any real use — these are still draft, non-binding figures, not a
|
||||
settled costing proposal. See `specs/PilotPhaseCandidateSurvey.md` for
|
||||
the underlying candidate rationale (unaffected by this figure update).
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
"source_revision": "f7ad73d"
|
||||
},
|
||||
"initial_target": {
|
||||
"amount": 2500000,
|
||||
"amount": 141800,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"target_basis": {
|
||||
"estimated_effort_days": 25,
|
||||
"estimated_effort_days": 1.418,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 0,
|
||||
"target_multiple": 100
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
"source_revision": "3890dca"
|
||||
},
|
||||
"initial_target": {
|
||||
"amount": 200000,
|
||||
"amount": 10000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"target_basis": {
|
||||
"estimated_effort_days": 20,
|
||||
"estimated_effort_days": 1.0,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 0,
|
||||
"target_multiple": 10
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
"source_revision": "398b0fe"
|
||||
},
|
||||
"initial_target": {
|
||||
"amount": 3500000,
|
||||
"amount": 648800,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"target_basis": {
|
||||
"estimated_effort_days": 30,
|
||||
"estimated_effort_days": 6.488,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 5000,
|
||||
"approved_direct_costs": 0,
|
||||
"target_multiple": 100
|
||||
},
|
||||
"future_license": "Apache-2.0",
|
||||
|
|
|
|||
193
history/260730-EffortCalculator-CandidateApplication.md
Normal file
193
history/260730-EffortCalculator-CandidateApplication.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Development Effort Calculator Applied to Real Pilot Candidates
|
||||
|
||||
**Document status:** Dated execution record, non-normative, append-only
|
||||
(`workplans/TREV-WP-0010-development-effort-calculator.md` T03).
|
||||
**Not a Phase declaration.** Updates draft, non-binding
|
||||
`examples/pilot-candidates/*/manifest.json` figures from hand-picked
|
||||
illustrative placeholders to calculator-derived ones — nothing here
|
||||
authorizes a real Phase (`workplans/TREV-WP-0008-governance-and-pilot-rollout.md`
|
||||
T05 still gates that).
|
||||
|
||||
---
|
||||
|
||||
## 1. Method
|
||||
|
||||
Ran `target_revenue.effort_calculator.calculate_target_basis` (WP-0010-T02)
|
||||
against each candidate's real local repository, `daily_rate=1000` (EUR,
|
||||
consistent with the earlier hand-picked drafts and the Licensor's own
|
||||
jurisdiction), `tokens_in=tokens_out=0` (no `get_token_summary` data was
|
||||
available for these repos at the time of this run — `approved_direct_costs`
|
||||
is therefore `0` for all three, not a claim that no AI cost was actually
|
||||
incurred, just that none is currently on record).
|
||||
|
||||
**Scoping decision, made explicit per candidate:** two of the four
|
||||
candidates repos are the *whole repo's* history (the repo's entire
|
||||
purpose is the candidate); two are one bounded workplan inside a much
|
||||
larger, longer-lived repo, and were date-scoped accordingly (`since`/
|
||||
`until`) — running whole-repo history against a single narrow workplan
|
||||
would badly overstate that workplan's own effort.
|
||||
|
||||
| Candidate | Repo | Scope used | Why |
|
||||
|---|---|---|---|
|
||||
| `net-kingdom-local-identity` | `net-kingdom` | `2026-03-01` .. `2026-03-06` | `NK-WP-0002` is one bounded workplan; `net-kingdom` has since grown into a much larger, multi-year security platform with dozens of unrelated workplans |
|
||||
| `railiance-vergabe-teilnahme` | `vergabe-teilnahme` (primary) + `railiance-apps` (secondary, scoped) | whole repo (primary); `2026-05-18` .. `2026-07-12` (secondary) | See §3 — the actual application code lives in `vergabe-teilnahme`, a separate repo from `railiance-apps`, which only holds the Helm/deployment wiring |
|
||||
| `info-tech-canon-service-surface` | `info-tech-canon` | whole repo | The candidate *is* the repo's cumulative service surface (`ITC-WP-0001`–`0012`); no narrower scope applies |
|
||||
|
||||
## 2. `net-kingdom-local-identity`
|
||||
|
||||
```json
|
||||
{
|
||||
"estimated_effort_days": 1.0,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 0.0,
|
||||
"warnings": [
|
||||
"Raw commit-clustered estimate was 0.804 day(s), below the 1.0-day floor. Floored to 1.0 day(s)... very likely a measurement gap... manual override... recommended.",
|
||||
"1 finished workplan(s) and 21 finished task(s) on record suggest more real effort than the 0.80-day raw time estimate reflects — treat the time estimate as under-counted and consider a manual override."
|
||||
],
|
||||
"derivation": {
|
||||
"raw_commit_clustered_hours": 6.435, "sessions": 4, "commits": 19,
|
||||
"finished_workplans": 1, "finished_tasks": 21,
|
||||
"file_count": 274, "line_count": 44359
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Both warnings fired.** The raw 0.8-day estimate hit the 1-day floor
|
||||
(§1's date-scoping correctly isolated `NK-WP-0002`'s own 19 commits across
|
||||
4 sessions), and the sanity-check independently flagged that 21 finished
|
||||
tasks across one workplan implies more real effort than 0.8 raw days
|
||||
suggests. **This is exactly the situation the maintainer's requested
|
||||
floor-and-warning design exists for**: the manifest below uses the
|
||||
1.0-day floored value, not a manually-adjusted "better" number — a human
|
||||
reviewing this candidate before any real Phase declaration should treat
|
||||
`estimated_effort_days: 1.0` as a flagged, likely-too-low placeholder, not
|
||||
a settled figure.
|
||||
|
||||
**Manifest updated:** `estimated_effort_days: 1.0`, `approved_direct_costs: 0`,
|
||||
`target_multiple: 10` (unchanged, Incremental) → **Initial Target: 10,000 EUR**
|
||||
(previously a hand-picked 200,000 EUR — the calculator's own honest,
|
||||
warning-flagged output is materially lower, not a refinement of the old
|
||||
guess).
|
||||
|
||||
## 3. `railiance-vergabe-teilnahme`
|
||||
|
||||
Two repos exist for this candidate, discovered during the earlier
|
||||
architecture work: `railiance-apps` holds the Helm/deployment wiring;
|
||||
`vergabe-teilnahme` holds the actual Django application source code.
|
||||
`RAILIANCE-WP-0002`/`-0014` (the workplans naming this Milestone Release)
|
||||
live in `railiance-apps`, but the real development effort producing the
|
||||
application itself happened in `vergabe-teilnahme`.
|
||||
|
||||
**Primary figure used — `vergabe-teilnahme`, whole-repo history (no
|
||||
scoping; the repo's entire purpose is this one application):**
|
||||
|
||||
```json
|
||||
{
|
||||
"estimated_effort_days": 6.488,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 0.0,
|
||||
"warnings": [],
|
||||
"derivation": {
|
||||
"raw_commit_clustered_hours": 51.904, "sessions": 42, "commits": 223,
|
||||
"finished_workplans": 1, "finished_tasks": 123,
|
||||
"file_count": 322, "line_count": 26773
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Secondary figure, not used in the manifest — `railiance-apps`, date-scoped
|
||||
to `2026-05-18`..`2026-07-12`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"estimated_effort_days": 4.146,
|
||||
"derivation": {
|
||||
"raw_commit_clustered_hours": 33.168, "sessions": 36, "commits": 127,
|
||||
"finished_workplans": 12, "finished_tasks": 71,
|
||||
"file_count": 140, "line_count": 11118
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Judgment call, made explicit rather than silently picked:** this
|
||||
document uses `vergabe-teilnahme`'s figure as the candidate's
|
||||
`target_basis`, since that repo is where the substantive product
|
||||
development happened; `railiance-apps`'s deployment effort (4.146 days)
|
||||
is real work but comparatively thin infrastructure glue, not the
|
||||
Milestone Release's own value creation. A different, equally defensible
|
||||
call would sum or otherwise combine both — flagged here so a human
|
||||
reviewing this later can see both numbers and the reasoning, not just
|
||||
the one that was picked.
|
||||
|
||||
**Manifest updated:** `estimated_effort_days: 6.488`,
|
||||
`approved_direct_costs: 0` (the hand-picked draft's `5000` placeholder
|
||||
removed — no real token-cost or other direct-cost data exists yet),
|
||||
`target_multiple: 100` (unchanged, Product-defining) → **Initial Target:
|
||||
648,800 EUR** (previously a hand-picked 3,500,000 EUR).
|
||||
|
||||
## 4. `info-tech-canon-service-surface`
|
||||
|
||||
```json
|
||||
{
|
||||
"estimated_effort_days": 1.418,
|
||||
"daily_rate": 1000,
|
||||
"approved_direct_costs": 0.0,
|
||||
"warnings": [
|
||||
"12 finished workplan(s) and 51 finished task(s) on record suggest more real effort than the 1.42-day raw time estimate reflects — treat the time estimate as under-counted and consider a manual override."
|
||||
],
|
||||
"derivation": {
|
||||
"raw_commit_clustered_hours": 11.348, "sessions": 12, "commits": 48,
|
||||
"finished_workplans": 12, "finished_tasks": 51,
|
||||
"file_count": 235, "line_count": 93663
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Above the 1-day floor (no floor warning), but the sanity-check warning
|
||||
still fired — 12 finished workplans and 51 finished tasks against only
|
||||
1.42 raw days is a clear under-count signal in its own right, independent
|
||||
of the floor. Same treatment as `net-kingdom`: the flagged, warning-
|
||||
carrying value is used as-is, not manually adjusted.
|
||||
|
||||
**Manifest updated:** `estimated_effort_days: 1.418`,
|
||||
`approved_direct_costs: 0`, `target_multiple: 100` (unchanged,
|
||||
Product-defining) → **Initial Target: 141,800 EUR** (previously a
|
||||
hand-picked 2,500,000 EUR).
|
||||
|
||||
## 5. A bug found and fixed along the way
|
||||
|
||||
Applying the calculator to `net-kingdom` surfaced a real defect in
|
||||
`workplan_task_counts()`: it originally scanned only the top level of
|
||||
`<repo>/workplans/`, non-recursively — and `NK-WP-0002` lives in
|
||||
`net-kingdom/workplans/archived/`, a convention several repos in this
|
||||
ecosystem use for finished workplans instead of deleting them. The
|
||||
initial scoped run silently reported zero finished workplans/tasks for a
|
||||
candidate that has exactly one, cleanly finished workplan. Fixed to scan
|
||||
recursively (`rglob` instead of `glob`); a regression test
|
||||
(`test_workplan_task_counts_scans_archived_subdirectory`) now guards this.
|
||||
|
||||
## 6. Summary table
|
||||
|
||||
| Candidate | Old (hand-picked) Initial Target | New (calculator-derived) Initial Target | Warnings fired |
|
||||
|---|---:|---:|---|
|
||||
| `net-kingdom-local-identity` | 200,000 EUR | **10,000 EUR** | Floor + sanity-check (both) |
|
||||
| `railiance-vergabe-teilnahme` | 3,500,000 EUR | **648,800 EUR** | None |
|
||||
| `info-tech-canon-service-surface` | 2,500,000 EUR | **141,800 EUR** | Sanity-check only |
|
||||
|
||||
Every calculator-derived figure is materially lower than the earlier
|
||||
hand-picked illustrative placeholder — expected, since those placeholders
|
||||
were invented for shape-checking the schema, not reasoned from any real
|
||||
measurement. Two of the three carry explicit warnings recommending a
|
||||
manual override before this could ever inform a real Phase declaration;
|
||||
the third (`vergabe-teilnahme`) has no warnings, but is itself the
|
||||
product of a judgment call (§3) about which of two repos to measure.
|
||||
|
||||
## 7. Non-goals
|
||||
|
||||
- Declaring any real Phase — `workplans/TREV-WP-0008-governance-and-pilot-rollout.md`
|
||||
T05 is unaffected by this document.
|
||||
- Resolving the `railiance-apps`/`vergabe-teilnahme` scoping judgment call
|
||||
with finality — flagged, not settled, in §3.
|
||||
- Sourcing real `get_token_summary` data for these three repos — not
|
||||
attempted in this pass; `approved_direct_costs: 0` reflects absence of
|
||||
data, not a claim that no AI cost was incurred.
|
||||
|
|
@ -113,7 +113,10 @@ class TargetBasisEstimate:
|
|||
|
||||
|
||||
def cluster_commit_hours(
|
||||
repo_path: str | Path, session_gap_hours: float = DEFAULT_SESSION_GAP_HOURS
|
||||
repo_path: str | Path,
|
||||
session_gap_hours: float = DEFAULT_SESSION_GAP_HOURS,
|
||||
since: str | None = None,
|
||||
until: str | None = None,
|
||||
) -> CommitTimeEstimate:
|
||||
"""Group commit timestamps into sessions (gap-based clustering).
|
||||
|
||||
|
|
@ -126,11 +129,20 @@ def cluster_commit_hours(
|
|||
This is an explicit floor estimate (concept §2a), not a true count —
|
||||
time spent reading, thinking, or working without committing is
|
||||
invisible to it by construction.
|
||||
|
||||
`since`/`until` (any `git log --since`/`--until`-accepted date string,
|
||||
e.g. `"2026-03-01"`) scope the estimate to a date range — required
|
||||
whenever the candidate Milestone Release is one bounded workplan
|
||||
within a repo whose overall history spans much more unrelated work
|
||||
than that one Phase. Whole-repo history (the default, no scoping) is
|
||||
only correct when the repo's entire history *is* the candidate.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repo_path), "log", "--all", "--format=%at"],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
cmd = ["git", "-C", str(repo_path), "log", "--all", "--format=%at"]
|
||||
if since:
|
||||
cmd += ["--since", since]
|
||||
if until:
|
||||
cmd += ["--until", until]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
timestamps = sorted(int(line) for line in result.stdout.splitlines() if line.strip())
|
||||
if not timestamps:
|
||||
return CommitTimeEstimate(raw_hours=0.0, session_count=0, commit_count=0)
|
||||
|
|
@ -161,16 +173,34 @@ def cluster_commit_hours(
|
|||
_TASK_BLOCK_RE = re.compile(r"```task\n(.*?)\n```", re.DOTALL)
|
||||
_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
|
||||
_STATUS_FIELD_RE = re.compile(r'^status:\s*"?([^"\n]+)"?\s*$', re.MULTILINE)
|
||||
_UPDATED_FIELD_RE = re.compile(r'^updated:\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2})"?\s*$', re.MULTILINE)
|
||||
_CREATED_FIELD_RE = re.compile(r'^created:\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2})"?\s*$', re.MULTILINE)
|
||||
|
||||
|
||||
def workplan_task_counts(repo_path: str | Path) -> WorkplanTaskCounts:
|
||||
def workplan_task_counts(
|
||||
repo_path: str | Path, since: str | None = None, until: str | None = None
|
||||
) -> WorkplanTaskCounts:
|
||||
"""Count finished workplans and tasks under `<repo>/workplans/`.
|
||||
|
||||
Follows this repo's own workplan convention (YAML-ish frontmatter with
|
||||
a `status:` field, ```task fenced blocks each with their own
|
||||
`status:` line) rather than requiring state-hub connectivity — works
|
||||
on any repo using this convention, degrades to zero (not an error) for
|
||||
repos with no `workplans/` directory at all.
|
||||
repos with no `workplans/` directory at all. Scans recursively (not
|
||||
just the top level), since several repos in this ecosystem archive
|
||||
finished workplans under `workplans/archived/` rather than deleting
|
||||
them — a non-recursive scan would silently undercount exactly the
|
||||
finished work this function exists to measure.
|
||||
|
||||
`since`/`until` (`YYYY-MM-DD` strings) restrict counting to workplans
|
||||
whose frontmatter `updated` (falling back to `created`) date falls
|
||||
within the window — a workplan file with neither field is always
|
||||
counted, since there is nothing to filter it by. Pass matching
|
||||
`since`/`until` values to `cluster_commit_hours` when scoping a
|
||||
candidate to one bounded workplan within a repo whose overall history
|
||||
spans much more; otherwise a whole-repo workplan count compared
|
||||
against a date-scoped time estimate produces a misleading sanity-check
|
||||
signal (falsely flagging under-counting).
|
||||
"""
|
||||
workplans_dir = Path(repo_path) / "workplans"
|
||||
if not workplans_dir.is_dir():
|
||||
|
|
@ -180,15 +210,26 @@ def workplan_task_counts(repo_path: str | Path) -> WorkplanTaskCounts:
|
|||
total_tasks = 0
|
||||
finished_tasks = 0
|
||||
|
||||
for md_file in sorted(workplans_dir.glob("*.md")):
|
||||
for md_file in sorted(workplans_dir.rglob("*.md")):
|
||||
try:
|
||||
text = md_file.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
frontmatter_match = _FRONTMATTER_RE.match(text)
|
||||
frontmatter = frontmatter_match.group(1) if frontmatter_match else ""
|
||||
|
||||
if since or until:
|
||||
date_match = _UPDATED_FIELD_RE.search(frontmatter) or _CREATED_FIELD_RE.search(frontmatter)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
if since and date_str < since:
|
||||
continue
|
||||
if until and date_str > until:
|
||||
continue
|
||||
|
||||
if frontmatter_match:
|
||||
status_match = _STATUS_FIELD_RE.search(frontmatter_match.group(1))
|
||||
status_match = _STATUS_FIELD_RE.search(frontmatter)
|
||||
if status_match and status_match.group(1).strip() == "finished":
|
||||
finished_workplans += 1
|
||||
|
||||
|
|
@ -336,6 +377,8 @@ def calculate_target_basis(
|
|||
hours_per_day: float = DEFAULT_HOURS_PER_DAY,
|
||||
extra_excludes: frozenset[str] = frozenset(),
|
||||
model_pricing: dict[str, dict[str, float]] | None = None,
|
||||
since: str | None = None,
|
||||
until: str | None = None,
|
||||
) -> TargetBasisEstimate:
|
||||
"""End-to-end convenience entry point: repo path + token counts in,
|
||||
a proposed `target_basis` estimate (with warnings and derivation) out.
|
||||
|
|
@ -344,9 +387,22 @@ def calculate_target_basis(
|
|||
hub's `get_token_summary`) rather than fetched here, since this
|
||||
module has no MCP/network client of its own — keeping it offline-first
|
||||
like the rest of this library.
|
||||
|
||||
`since`/`until` scope both commit-time clustering and workplan/task
|
||||
counting to the same date window — pass both whenever the candidate
|
||||
Milestone Release is one bounded workplan within a repo whose overall
|
||||
history spans much more (see `cluster_commit_hours`/
|
||||
`workplan_task_counts` docstrings); leave unset only when the repo's
|
||||
entire history genuinely is the candidate. Repo size (file/line
|
||||
counts) is not date-scoped — it reflects the repo's current state
|
||||
regardless of when each file was last touched, which is intentional
|
||||
(`target_basis` describes the Milestone Release's actual delivered
|
||||
size, not a historical snapshot) but worth noting as an asymmetry.
|
||||
"""
|
||||
commit_time = cluster_commit_hours(repo_path, session_gap_hours=session_gap_hours)
|
||||
workplan_tasks = workplan_task_counts(repo_path)
|
||||
commit_time = cluster_commit_hours(
|
||||
repo_path, session_gap_hours=session_gap_hours, since=since, until=until
|
||||
)
|
||||
workplan_tasks = workplan_task_counts(repo_path, since=since, until=until)
|
||||
repo_size = repo_size_metrics(repo_path, extra_excludes=extra_excludes)
|
||||
usd = token_cost_usd(tokens_in, tokens_out, model=model, model_pricing=model_pricing)
|
||||
token_cost = TokenCost(tokens_in=tokens_in, tokens_out=tokens_out, usd=usd)
|
||||
|
|
|
|||
|
|
@ -86,6 +86,27 @@ def test_cluster_commit_hours_gap_splits_into_two_sessions(git_repo):
|
|||
# --- workplan_task_counts ------------------------------------------------
|
||||
|
||||
|
||||
def test_workplan_task_counts_scans_archived_subdirectory(tmp_path):
|
||||
archived = tmp_path / "workplans" / "archived"
|
||||
archived.mkdir(parents=True)
|
||||
(archived / "old-finished.md").write_text(
|
||||
"""---
|
||||
id: OLD-0001
|
||||
status: finished
|
||||
---
|
||||
|
||||
```task
|
||||
id: OLD-0001-T01
|
||||
status: done
|
||||
```
|
||||
"""
|
||||
)
|
||||
result = ec.workplan_task_counts(tmp_path)
|
||||
assert result.finished_workplans == 1
|
||||
assert result.total_tasks == 1
|
||||
assert result.finished_tasks == 1
|
||||
|
||||
|
||||
def test_workplan_task_counts_no_workplans_dir(tmp_path):
|
||||
result = ec.workplan_task_counts(tmp_path)
|
||||
assert result == ec.WorkplanTaskCounts(0, 0, 0)
|
||||
|
|
@ -244,3 +265,78 @@ def test_calculate_target_basis_end_to_end(git_repo):
|
|||
ec.token_cost_usd(1000, 1000), abs=0.01
|
||||
)
|
||||
assert isinstance(result.estimated_effort_days, float)
|
||||
|
||||
|
||||
# --- since/until date-range scoping ---------------------------------------
|
||||
|
||||
|
||||
def test_cluster_commit_hours_since_until_excludes_outside_window(git_repo):
|
||||
_commit(git_repo, "outside-early", 1_577_836_800) # 2020-01-01
|
||||
_commit(git_repo, "inside", 1_614_556_800) # 2021-03-01
|
||||
_commit(git_repo, "outside-late", 1_672_531_200) # 2023-01-01
|
||||
result = ec.cluster_commit_hours(git_repo, since="2021-01-01", until="2021-06-01")
|
||||
assert result.commit_count == 1
|
||||
|
||||
|
||||
def test_workplan_task_counts_since_until_filters_by_updated_date(tmp_path):
|
||||
workplans = tmp_path / "workplans"
|
||||
workplans.mkdir()
|
||||
(workplans / "old.md").write_text(
|
||||
"""---
|
||||
id: OLD-0001
|
||||
status: finished
|
||||
updated: "2020-01-01"
|
||||
---
|
||||
|
||||
```task
|
||||
id: OLD-0001-T01
|
||||
status: done
|
||||
```
|
||||
"""
|
||||
)
|
||||
(workplans / "recent.md").write_text(
|
||||
"""---
|
||||
id: NEW-0001
|
||||
status: finished
|
||||
updated: "2026-03-03"
|
||||
---
|
||||
|
||||
```task
|
||||
id: NEW-0001-T01
|
||||
status: done
|
||||
```
|
||||
"""
|
||||
)
|
||||
result = ec.workplan_task_counts(tmp_path, since="2026-01-01", until="2026-06-01")
|
||||
assert result.finished_workplans == 1
|
||||
assert result.total_tasks == 1
|
||||
|
||||
|
||||
def test_workplan_task_counts_no_date_field_always_counted(tmp_path):
|
||||
workplans = tmp_path / "workplans"
|
||||
workplans.mkdir()
|
||||
(workplans / "undated.md").write_text(
|
||||
"""---
|
||||
id: UND-0001
|
||||
status: finished
|
||||
---
|
||||
|
||||
```task
|
||||
id: UND-0001-T01
|
||||
status: done
|
||||
```
|
||||
"""
|
||||
)
|
||||
result = ec.workplan_task_counts(tmp_path, since="2026-01-01", until="2026-06-01")
|
||||
assert result.finished_workplans == 1
|
||||
assert result.total_tasks == 1
|
||||
|
||||
|
||||
def test_calculate_target_basis_threads_since_until(git_repo):
|
||||
base = 1_577_836_800 # 2020-01-01
|
||||
_commit(git_repo, "outside", base)
|
||||
_commit(git_repo, "inside", base + 86400 * 400) # ~2021-02-04
|
||||
result = ec.calculate_target_basis(
|
||||
git_repo, daily_rate=1000.0, since="2021-01-01", until="2021-06-01"
|
||||
)
|
||||
assert result.derivation["commits"] == 1
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Development Effort Calculator"
|
||||
domain: infotech
|
||||
repo: target-revenue
|
||||
status: active
|
||||
status: finished
|
||||
owner: claude
|
||||
topic_slug: infotech
|
||||
created: "2026-07-30"
|
||||
|
|
@ -153,7 +153,7 @@ new hard dependency (stdlib + existing `pathlib`/`subprocess`/`re` only).
|
|||
|
||||
```task
|
||||
id: TREV-WP-0010-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "74a3db97-1b66-4514-895a-bb9c385008aa"
|
||||
```
|
||||
|
|
@ -167,3 +167,31 @@ illustrative figures currently in `examples/pilot-candidates/*/manifest.json`.
|
|||
This remains draft/non-binding — updating a draft manifest's numbers is
|
||||
not a Phase declaration, and does not touch
|
||||
`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T05's own gate.
|
||||
|
||||
**Result:** All three draft manifests updated with calculator-derived
|
||||
`target_basis`/`initial_target` values; full derivation and warnings in
|
||||
`history/260730-EffortCalculator-CandidateApplication.md`.
|
||||
`net-kingdom-local-identity`: date-scoped to `NK-WP-0002`'s own window
|
||||
(`net-kingdom` has since grown far beyond that one workplan) — hit the
|
||||
1-day floor *and* the sanity-check warning; Initial Target 200,000 → 10,000
|
||||
EUR. `railiance-vergabe-teilnahme`: measured against the `vergabe-teilnahme`
|
||||
application repo itself (not `railiance-apps`, which only holds
|
||||
deployment wiring — a judgment call recorded explicitly, with the
|
||||
alternative repo's figure shown too) — no warnings; Initial Target
|
||||
3,500,000 → 648,800 EUR. `info-tech-canon-service-surface`: whole-repo
|
||||
history — sanity-check warning fired (12 finished workplans/51 tasks
|
||||
against 1.42 raw days) even though above the 1-day floor; Initial Target
|
||||
2,500,000 → 141,800 EUR. Every calculator-derived figure came out
|
||||
materially lower than the earlier hand-picked placeholders, as expected
|
||||
since those were never reasoned from a real measurement.
|
||||
|
||||
**Bug found and fixed along the way:** `workplan_task_counts()` only
|
||||
scanned the top level of `workplans/`, missing `net-kingdom`'s
|
||||
`workplans/archived/` convention entirely (silently reporting zero
|
||||
finished workplans for `NK-WP-0002`, which lives there). Fixed to scan
|
||||
recursively; added a regression test
|
||||
(`test_workplan_task_counts_scans_archived_subdirectory`). Also added
|
||||
`since`/`until` date-range scoping to both `cluster_commit_hours()` and
|
||||
`workplan_task_counts()` (threaded through `calculate_target_basis()`),
|
||||
needed for this task and reusable for any future candidate that's one
|
||||
workplan within a larger repo.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue