roadmodel 0.2.96__tar.gz → 0.2.97__tar.gz
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.
- {roadmodel-0.2.96 → roadmodel-0.2.97}/PKG-INFO +1 -1
- {roadmodel-0.2.96/roadmodel/data → roadmodel-0.2.97/docs/claude-commands}/roadmap-refresh.md +9 -1
- {roadmodel-0.2.96/roadmodel/data → roadmodel-0.2.97/docs/claude-commands}/roadmap-step.md +7 -2
- {roadmodel-0.2.96/roadmodel/data → roadmodel-0.2.97/docs/templates}/phase-roadmap-template.md +12 -5
- {roadmodel-0.2.96 → roadmodel-0.2.97}/pyproject.toml +1 -1
- {roadmodel-0.2.96/docs/templates → roadmodel-0.2.97/roadmodel/data}/phase-roadmap-template.md +12 -5
- {roadmodel-0.2.96/docs/claude-commands → roadmodel-0.2.97/roadmodel/data}/roadmap-refresh.md +9 -1
- {roadmodel-0.2.96/docs/claude-commands → roadmodel-0.2.97/roadmodel/data}/roadmap-step.md +7 -2
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/__init__.py +1 -1
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/cli.py +174 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/ladder_layout.py +156 -3
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/scoring.py +78 -81
- {roadmodel-0.2.96 → roadmodel-0.2.97}/.github/README.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/.gitignore +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/LICENSE +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/NOTICE +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/README.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/benchmarks.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/catalog.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/claude-commands/roadmap-phase.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/claude-commands/roadmap-project.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/claude-commands/roadmodel-upgrade.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/context-windows.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/effort-tokens.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/model-selector.txt +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/model-tier-cost-scale.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/settings-display.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/templates/planning-kit-how-to-use.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/templates/project-roadmap-template.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/templates/prompt-phase-roadmap.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/templates/prompt-project-roadmap.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/docs/user-context.example.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/hatch_build.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/infra/README.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/infra/gcp-killswitch/README.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/infra/supabase/README.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/benchmarks.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/catalog.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/context-windows.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/effort-tokens.json +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/model-selector.txt +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/model-tier-cost-scale.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/planning-kit-how-to-use.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/project-roadmap-template.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/prompt-phase-roadmap.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/prompt-project-roadmap.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/roadmap-phase.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/roadmap-project.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/roadmodel-upgrade.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/settings-display.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/roadmodel/data/user-context.example.md +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/agent_context.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/agent_rules.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/update_projects.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/updater_lock.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/upgrade_fleet.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/scripts/worktree_memory.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/__main__.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/config.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/cost.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/errors.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/mcp_server.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/private_files.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/__init__.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/_safety.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/anthropic.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/google.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/openai.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/openai_compatible.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/providers/registry.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/recommend.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/series.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/usage.py +0 -0
- {roadmodel-0.2.96 → roadmodel-0.2.97}/src/roadmodel/user_context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: roadmodel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.97
|
|
4
4
|
Summary: BYO-key CLI that recommends the right AI model, platform, and settings for a prompt.
|
|
5
5
|
Project-URL: Homepage, https://roadmodel.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/nathanramoscfa/roadmodel
|
{roadmodel-0.2.96/roadmodel/data → roadmodel-0.2.97/docs/claude-commands}/roadmap-refresh.md
RENAMED
|
@@ -221,7 +221,15 @@ complexity, novel or not; its Model rationale names them), then:
|
|
|
221
221
|
not exist: a Complete step's ladder is the record of what ran, an `In
|
|
222
222
|
progress` step's is what an agent is running, and a written phase's
|
|
223
223
|
records what wrote its roadmap, so those keep their layout. That is a
|
|
224
|
-
layout fix, not a new pick, and takes no `Settings updated` line.
|
|
224
|
+
layout fix, not a new pick, and takes no `Settings updated` line. Then
|
|
225
|
+
run `roadmodel rerank <every roadmap file>` (roadmodel 0.2.97 on): it
|
|
226
|
+
gives each of those ladder tables the ranks this release computes, so a
|
|
227
|
+
change to how roadmodel ranks reaches every step not yet run. It reads
|
|
228
|
+
the classification from the ladder's note (`Ranked for …`), else from the
|
|
229
|
+
only classification whose ladder has the same Picks. It adds a `Settings
|
|
230
|
+
updated` line where a rank changed. It reports any ladder it cannot
|
|
231
|
+
place: classify that step from its Model rationale and re-run with
|
|
232
|
+
`--hint FILE:SECTION=category/complexity[/novel]`. A
|
|
225
233
|
ladder written before roadmodel 0.2.93 has Model / Backup / Platform /
|
|
226
234
|
Effort (/ AA Index) / Thinking rows and no Runner-up: on a step that has
|
|
227
235
|
not shipped, or a phase not yet written, replace it with the ladder
|
|
@@ -115,7 +115,11 @@ the dials and make sure it is set.
|
|
|
115
115
|
Price rows. Level is the bare level of that platform's own dial (`High`
|
|
116
116
|
is Intelligence High on Codex, Effort High on Claude Code and
|
|
117
117
|
Antigravity). The ✓ in a column's header marks the one its **Runs:**
|
|
118
|
-
line names.
|
|
118
|
+
line names. From roadmodel 0.2.97 the three ranks of a column come from
|
|
119
|
+
three makers (a tight pool is a maker's); where too few are funded, a
|
|
120
|
+
missing rank is the Pick one level lower, its Level marked ↓, and the
|
|
121
|
+
note line says which classification the ladder was ranked for. A 0.2.94
|
|
122
|
+
ladder is three lists instead (`- Pick:`,
|
|
119
123
|
`- Runner-up:`, `- Backup:` under each column's bold heading), and a
|
|
120
124
|
0.2.93 one a table with rows `1 · Pick`, `2 · Runner-up` and
|
|
121
125
|
`3 · Backup`, each followed by Platform, Effort, Thinking and AA Index
|
|
@@ -129,7 +133,8 @@ the dials and make sure it is set.
|
|
|
129
133
|
subscription that pays for that column's Pick's platform:
|
|
130
134
|
- `headroom`: run that column's Pick.
|
|
131
135
|
- `tight`: run the first of that column's Runner-up and Backup whose
|
|
132
|
-
pool reads `headroom
|
|
136
|
+
pool reads `headroom` or that is marked ↓ (the Pick one level lower,
|
|
137
|
+
which uses less of the tight pool), else the Cost column's Pick.
|
|
133
138
|
- `exhausted`: run the first of that column's Runner-up and Backup
|
|
134
139
|
whose pool reads `headroom`, else its Backup.
|
|
135
140
|
|
{roadmodel-0.2.96/roadmodel/data → roadmodel-0.2.97/docs/templates}/phase-roadmap-template.md
RENAMED
|
@@ -774,11 +774,18 @@ that owns it.}}
|
|
|
774
774
|
platform has the toggle, and every AA Index and Price
|
|
775
775
|
already assumes it. A note line under the table explains
|
|
776
776
|
✓ and Price, and a `**Conversation:**` line closes the
|
|
777
|
-
ladder.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
the
|
|
781
|
-
|
|
777
|
+
ladder. Each rank comes from its own maker (roadmodel
|
|
778
|
+
0.2.97 on), because a tight usage pool is a maker's: the
|
|
779
|
+
Runner-up is the column's recommendation with the Pick's
|
|
780
|
+
maker left out, and the Backup leaves out both makers
|
|
781
|
+
above it. Where the user-context funds too few makers, a
|
|
782
|
+
missing rank is the Pick's own model one level lower,
|
|
783
|
+
its Level marked ↓ (it uses less of a tight pool), and
|
|
784
|
+
`none` below the dial's lowest level. The note line
|
|
785
|
+
names the classification the ladder was ranked for. The
|
|
786
|
+
ranks follow the column's own rule, not the AA Index:
|
|
787
|
+
Cost ranks each maker's cheapest adequate option, so its
|
|
788
|
+
AA Index may rise down the column. Every model is the latest
|
|
782
789
|
version of its series, never an older one (GPT-6 Sol
|
|
783
790
|
where GPT-6.1 Sol runs, Opus 5 where Opus 5.5 does).
|
|
784
791
|
Set the Conversation line to **Continue** only when the
|
{roadmodel-0.2.96/docs/templates → roadmodel-0.2.97/roadmodel/data}/phase-roadmap-template.md
RENAMED
|
@@ -774,11 +774,18 @@ that owns it.}}
|
|
|
774
774
|
platform has the toggle, and every AA Index and Price
|
|
775
775
|
already assumes it. A note line under the table explains
|
|
776
776
|
✓ and Price, and a `**Conversation:**` line closes the
|
|
777
|
-
ladder.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
the
|
|
781
|
-
|
|
777
|
+
ladder. Each rank comes from its own maker (roadmodel
|
|
778
|
+
0.2.97 on), because a tight usage pool is a maker's: the
|
|
779
|
+
Runner-up is the column's recommendation with the Pick's
|
|
780
|
+
maker left out, and the Backup leaves out both makers
|
|
781
|
+
above it. Where the user-context funds too few makers, a
|
|
782
|
+
missing rank is the Pick's own model one level lower,
|
|
783
|
+
its Level marked ↓ (it uses less of a tight pool), and
|
|
784
|
+
`none` below the dial's lowest level. The note line
|
|
785
|
+
names the classification the ladder was ranked for. The
|
|
786
|
+
ranks follow the column's own rule, not the AA Index:
|
|
787
|
+
Cost ranks each maker's cheapest adequate option, so its
|
|
788
|
+
AA Index may rise down the column. Every model is the latest
|
|
782
789
|
version of its series, never an older one (GPT-6 Sol
|
|
783
790
|
where GPT-6.1 Sol runs, Opus 5 where Opus 5.5 does).
|
|
784
791
|
Set the Conversation line to **Continue** only when the
|
{roadmodel-0.2.96/docs/claude-commands → roadmodel-0.2.97/roadmodel/data}/roadmap-refresh.md
RENAMED
|
@@ -221,7 +221,15 @@ complexity, novel or not; its Model rationale names them), then:
|
|
|
221
221
|
not exist: a Complete step's ladder is the record of what ran, an `In
|
|
222
222
|
progress` step's is what an agent is running, and a written phase's
|
|
223
223
|
records what wrote its roadmap, so those keep their layout. That is a
|
|
224
|
-
layout fix, not a new pick, and takes no `Settings updated` line.
|
|
224
|
+
layout fix, not a new pick, and takes no `Settings updated` line. Then
|
|
225
|
+
run `roadmodel rerank <every roadmap file>` (roadmodel 0.2.97 on): it
|
|
226
|
+
gives each of those ladder tables the ranks this release computes, so a
|
|
227
|
+
change to how roadmodel ranks reaches every step not yet run. It reads
|
|
228
|
+
the classification from the ladder's note (`Ranked for …`), else from the
|
|
229
|
+
only classification whose ladder has the same Picks. It adds a `Settings
|
|
230
|
+
updated` line where a rank changed. It reports any ladder it cannot
|
|
231
|
+
place: classify that step from its Model rationale and re-run with
|
|
232
|
+
`--hint FILE:SECTION=category/complexity[/novel]`. A
|
|
225
233
|
ladder written before roadmodel 0.2.93 has Model / Backup / Platform /
|
|
226
234
|
Effort (/ AA Index) / Thinking rows and no Runner-up: on a step that has
|
|
227
235
|
not shipped, or a phase not yet written, replace it with the ladder
|
|
@@ -115,7 +115,11 @@ the dials and make sure it is set.
|
|
|
115
115
|
Price rows. Level is the bare level of that platform's own dial (`High`
|
|
116
116
|
is Intelligence High on Codex, Effort High on Claude Code and
|
|
117
117
|
Antigravity). The ✓ in a column's header marks the one its **Runs:**
|
|
118
|
-
line names.
|
|
118
|
+
line names. From roadmodel 0.2.97 the three ranks of a column come from
|
|
119
|
+
three makers (a tight pool is a maker's); where too few are funded, a
|
|
120
|
+
missing rank is the Pick one level lower, its Level marked ↓, and the
|
|
121
|
+
note line says which classification the ladder was ranked for. A 0.2.94
|
|
122
|
+
ladder is three lists instead (`- Pick:`,
|
|
119
123
|
`- Runner-up:`, `- Backup:` under each column's bold heading), and a
|
|
120
124
|
0.2.93 one a table with rows `1 · Pick`, `2 · Runner-up` and
|
|
121
125
|
`3 · Backup`, each followed by Platform, Effort, Thinking and AA Index
|
|
@@ -129,7 +133,8 @@ the dials and make sure it is set.
|
|
|
129
133
|
subscription that pays for that column's Pick's platform:
|
|
130
134
|
- `headroom`: run that column's Pick.
|
|
131
135
|
- `tight`: run the first of that column's Runner-up and Backup whose
|
|
132
|
-
pool reads `headroom
|
|
136
|
+
pool reads `headroom` or that is marked ↓ (the Pick one level lower,
|
|
137
|
+
which uses less of the tight pool), else the Cost column's Pick.
|
|
133
138
|
- `exhausted`: run the first of that column's Runner-up and Backup
|
|
134
139
|
whose pool reads `headroom`, else its Backup.
|
|
135
140
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# src/roadmodel/__init__.py
|
|
2
|
-
__version__ = "0.2.
|
|
2
|
+
__version__ = "0.2.97"
|
|
@@ -730,6 +730,180 @@ def relayout_command(paths: tuple[Path, ...], check: bool) -> None:
|
|
|
730
730
|
raise SystemExit(1)
|
|
731
731
|
|
|
732
732
|
|
|
733
|
+
@cli.command("rerank")
|
|
734
|
+
@click.argument(
|
|
735
|
+
"paths",
|
|
736
|
+
nargs=-1,
|
|
737
|
+
required=True,
|
|
738
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
739
|
+
)
|
|
740
|
+
@click.option(
|
|
741
|
+
"--check",
|
|
742
|
+
is_flag=True,
|
|
743
|
+
default=False,
|
|
744
|
+
help="Report the ladders without rewriting them; exit 1 if any would change.",
|
|
745
|
+
)
|
|
746
|
+
@click.option(
|
|
747
|
+
"--hint",
|
|
748
|
+
"hints",
|
|
749
|
+
multiple=True,
|
|
750
|
+
metavar="FILE:SECTION=CATEGORY/COMPLEXITY[/novel]",
|
|
751
|
+
help="Classification for a ladder whose note names none and whose Picks match "
|
|
752
|
+
"several (e.g. 'phase31.6-roadmap.md:Step 5=coding/high').",
|
|
753
|
+
)
|
|
754
|
+
@click.option(
|
|
755
|
+
"--user-context",
|
|
756
|
+
"user_context_path",
|
|
757
|
+
type=click.Path(path_type=Path),
|
|
758
|
+
default=None,
|
|
759
|
+
help="Path to user-context.md override.",
|
|
760
|
+
)
|
|
761
|
+
def rerank_command(
|
|
762
|
+
paths: tuple[Path, ...], check: bool, hints: tuple[str, ...], user_context_path: Path | None
|
|
763
|
+
) -> None:
|
|
764
|
+
"""Re-rank each roadmap's ladder tables by this release's rules.
|
|
765
|
+
|
|
766
|
+
For every ladder table under a step that has not started, or a phase whose
|
|
767
|
+
roadmap does not exist, prints the ladder this release ranks for the same
|
|
768
|
+
classification: the one its note names, else the one a --hint gives, else
|
|
769
|
+
the only one whose Picks match the table's. A ladder whose Runner-up or
|
|
770
|
+
Backup changes gains a `> Settings updated` line beneath it. Complete and
|
|
771
|
+
In progress steps, and written phases, keep their ladder; a ladder whose
|
|
772
|
+
classification stays unclear is reported and left as it is.
|
|
773
|
+
"""
|
|
774
|
+
import datetime as _dt
|
|
775
|
+
from collections import Counter
|
|
776
|
+
|
|
777
|
+
from roadmodel import ladder_layout
|
|
778
|
+
|
|
779
|
+
text_ctx = _scoring_user_context(user_context_path)
|
|
780
|
+
budget = scoring.declared_budget(text_ctx).lower()
|
|
781
|
+
cache: dict[tuple[tuple[str, str, bool], bool], dict[str, Any] | None] = {}
|
|
782
|
+
|
|
783
|
+
def ladder_for(key: tuple[str, str, bool], phase: bool) -> dict[str, Any] | None:
|
|
784
|
+
if (key, phase) not in cache:
|
|
785
|
+
task = scoring.Task(key[0], key[1], key[2], budget)
|
|
786
|
+
cache[(key, phase)] = scoring.settings_ladder(task, text_ctx, phase=phase)
|
|
787
|
+
return cache[(key, phase)]
|
|
788
|
+
|
|
789
|
+
def picks(got: dict[str, Any]) -> tuple[tuple[str, str], ...]:
|
|
790
|
+
return tuple(
|
|
791
|
+
(str(got["columns"][t]["model"]), str(got["columns"][t]["level_cell"]))
|
|
792
|
+
for t in scoring.LADDER_TIERS
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def ranks(got: dict[str, Any]) -> tuple[tuple[str, ...], ...]:
|
|
796
|
+
cols = [got["columns"][t] for t in scoring.LADDER_TIERS]
|
|
797
|
+
return tuple(
|
|
798
|
+
tuple("none" if o is None else str(o["model"]) for o in opts)
|
|
799
|
+
for opts in (cols, [c["runner_up"] for c in cols], [c["backup"] for c in cols])
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
hinted: dict[tuple[str, str], tuple[str, str, bool]] = {}
|
|
803
|
+
for hint in hints:
|
|
804
|
+
where, _, what = hint.partition("=")
|
|
805
|
+
name, _, section = where.partition(":")
|
|
806
|
+
parts = what.strip().lower().split("/")
|
|
807
|
+
if len(parts) not in (2, 3) or not name or not section:
|
|
808
|
+
raise click.ClickException(f"--hint {hint!r}: use FILE:SECTION=CATEGORY/COMPLEXITY")
|
|
809
|
+
hinted[(name.strip(), section.strip())] = (parts[0], parts[1], parts[2:] == ["novel"])
|
|
810
|
+
keys = scoring.table_keys()
|
|
811
|
+
today = _dt.date.today().isoformat()
|
|
812
|
+
pending = 0
|
|
813
|
+
unresolved: list[str] = []
|
|
814
|
+
for path in paths:
|
|
815
|
+
text = path.read_text(encoding="utf-8")
|
|
816
|
+
lines = text.split("\n")
|
|
817
|
+
counts: Counter[str] = Counter()
|
|
818
|
+
replacements: dict[int, tuple[int, str]] = {}
|
|
819
|
+
for block in ladder_layout.table_ladders(text, phase_written=_phase_written(path.parent)):
|
|
820
|
+
if block.done:
|
|
821
|
+
counts["kept"] += 1
|
|
822
|
+
continue
|
|
823
|
+
key = block.ranked_for or hinted.get((path.name, block.section))
|
|
824
|
+
if key is None:
|
|
825
|
+
matches = [
|
|
826
|
+
k for k in keys if (g := ladder_for(k, block.phase)) and picks(g) == block.picks
|
|
827
|
+
]
|
|
828
|
+
tables = {
|
|
829
|
+
str((ladder_for(k, block.phase) or {})["markdown"]).split("\n\n")[0]
|
|
830
|
+
for k in matches
|
|
831
|
+
}
|
|
832
|
+
if len(tables) != 1:
|
|
833
|
+
counts["unresolved"] += 1
|
|
834
|
+
found = ", ".join(
|
|
835
|
+
"/".join(map(str, k[:2])) + ("/novel" if k[2] else "") for k in matches
|
|
836
|
+
)
|
|
837
|
+
unresolved.append(
|
|
838
|
+
f"{path}: {block.section or 'ladder'} — "
|
|
839
|
+
+ (
|
|
840
|
+
f"Picks match {found}"
|
|
841
|
+
if matches
|
|
842
|
+
else "no classification reproduces its Picks"
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
continue
|
|
846
|
+
key = matches[0]
|
|
847
|
+
guessed = len(matches) > 1
|
|
848
|
+
else:
|
|
849
|
+
guessed = False
|
|
850
|
+
got = ladder_for(key, block.phase)
|
|
851
|
+
if got is None:
|
|
852
|
+
counts["unresolved"] += 1
|
|
853
|
+
unresolved.append(f"{path}: {block.section or 'ladder'} — no ladder for {key}")
|
|
854
|
+
continue
|
|
855
|
+
md = str(got["markdown"])
|
|
856
|
+
if guessed:
|
|
857
|
+
# Several classifications rank this ladder alike: the table is
|
|
858
|
+
# right, but naming one of them in the note would be a guess.
|
|
859
|
+
md = _drop_ranked_for(md)
|
|
860
|
+
if block.conversation and block.conversation != "New":
|
|
861
|
+
md = md.replace(
|
|
862
|
+
"**Conversation:** New", f"**Conversation:** {block.conversation}", 1
|
|
863
|
+
)
|
|
864
|
+
if md == "\n".join(lines[block.start : block.end]):
|
|
865
|
+
counts["unchanged"] += 1
|
|
866
|
+
continue
|
|
867
|
+
if picks(got) != block.picks:
|
|
868
|
+
runs = scoring.LADDER_TIERS.index(str(got["runs"]))
|
|
869
|
+
was = " · ".join(block.picks[runs]) if len(block.picks) > runs else "—"
|
|
870
|
+
md += (
|
|
871
|
+
f"\n\n> Settings updated {today} (rerank): was {was}. Re-ranked for "
|
|
872
|
+
f"{key[0]}, {key[1]} complexity, each column's ranks from three makers."
|
|
873
|
+
)
|
|
874
|
+
elif ranks(got) != block.ranks:
|
|
875
|
+
md += (
|
|
876
|
+
f"\n\n> Settings updated {today} (rerank): each column's Runner-up and "
|
|
877
|
+
"Backup now come from makers other than its Pick; every Pick is unchanged."
|
|
878
|
+
)
|
|
879
|
+
replacements[block.start] = (block.end, md)
|
|
880
|
+
counts["reranked"] += 1
|
|
881
|
+
if replacements and not check:
|
|
882
|
+
path.write_text(ladder_layout.replace_ladders(text, replacements), encoding="utf-8")
|
|
883
|
+
pending += len(replacements)
|
|
884
|
+
if counts:
|
|
885
|
+
click.echo(
|
|
886
|
+
f"{path}: {counts['reranked']} re-ranked, {counts['unchanged']} unchanged, "
|
|
887
|
+
f"{counts['kept']} kept, {counts['unresolved']} unresolved"
|
|
888
|
+
)
|
|
889
|
+
for line in unresolved:
|
|
890
|
+
click.echo(f" unresolved {line}; pass --hint or run /roadmap-refresh")
|
|
891
|
+
if check and pending:
|
|
892
|
+
raise SystemExit(1)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _drop_ranked_for(markdown: str) -> str:
|
|
896
|
+
"""``markdown`` with its note's "Ranked for …" sentence removed and the
|
|
897
|
+
note re-wrapped at 80 columns."""
|
|
898
|
+
parts = markdown.split("\n\n")
|
|
899
|
+
for i, part in enumerate(parts):
|
|
900
|
+
flat = " ".join(part.split())
|
|
901
|
+
if flat.startswith("Ranked for "):
|
|
902
|
+
rest = re.sub(r"^Ranked for [^.]*\.\s*", "", flat)
|
|
903
|
+
parts[i] = textwrap.fill(rest, width=80, break_long_words=False)
|
|
904
|
+
return "\n\n".join(p for p in parts if p)
|
|
905
|
+
|
|
906
|
+
|
|
733
907
|
def _phase_written(directory: Path) -> Callable[[str], bool]:
|
|
734
908
|
"""Whether a phase's roadmap exists beside the project roadmap:
|
|
735
909
|
``phase<id>-roadmap.md`` or ``phase<id>-<slug>-roadmap.md``, the id as the
|
|
@@ -42,6 +42,7 @@ RANKS: Final[tuple[tuple[str, str], ...]] = (
|
|
|
42
42
|
FIELDS: Final[tuple[str, ...]] = ("Model", "Platform", "Level", "AA", "Price")
|
|
43
43
|
INDENT: Final[str] = " " * 4
|
|
44
44
|
RUNS_MARK: Final[str] = "✓"
|
|
45
|
+
STEP_MARK: Final[str] = "↓"
|
|
45
46
|
WIDTH: Final[int] = 80
|
|
46
47
|
NONE: Final[str] = "—"
|
|
47
48
|
_SEP: Final[str] = " · "
|
|
@@ -81,6 +82,9 @@ class Option:
|
|
|
81
82
|
price: float | None = None
|
|
82
83
|
# False when the level's token use is a typical model's, not measured.
|
|
83
84
|
price_measured: bool = True
|
|
85
|
+
# The Pick's own model a level or two lower, filling a rank no other
|
|
86
|
+
# maker could (marked ↓).
|
|
87
|
+
stepped_down: bool = False
|
|
84
88
|
|
|
85
89
|
|
|
86
90
|
def level_key(word: str) -> str | None:
|
|
@@ -100,7 +104,7 @@ def _cells(option: Option | None) -> list[str]:
|
|
|
100
104
|
return [
|
|
101
105
|
f"**{option.model}**",
|
|
102
106
|
option.platform,
|
|
103
|
-
option.level,
|
|
107
|
+
option.level + (f" {STEP_MARK}" if option.stepped_down else ""),
|
|
104
108
|
NONE if option.aa is None else option.aa,
|
|
105
109
|
price_cell(option),
|
|
106
110
|
]
|
|
@@ -120,6 +124,7 @@ def ladder_markdown(
|
|
|
120
124
|
*,
|
|
121
125
|
runs: str | None,
|
|
122
126
|
conversation: str | None,
|
|
127
|
+
ranked_for: str | None = None,
|
|
123
128
|
) -> str:
|
|
124
129
|
"""The ladder's table, its note, then its Conversation line. A rank is
|
|
125
130
|
printed when any column carries it (a ladder older than the Runner-up has
|
|
@@ -128,7 +133,7 @@ def ladder_markdown(
|
|
|
128
133
|
column reads `none` alone)."""
|
|
129
134
|
head = ["", *(t.capitalize() + (f" {RUNS_MARK}" if t == runs else "") for t in TIERS)]
|
|
130
135
|
rows = [head]
|
|
131
|
-
priced = unmeasured = False
|
|
136
|
+
priced = unmeasured = stepped = False
|
|
132
137
|
for label, key in RANKS:
|
|
133
138
|
if not any(key in columns[t] for t in TIERS):
|
|
134
139
|
continue
|
|
@@ -144,7 +149,15 @@ def ladder_markdown(
|
|
|
144
149
|
unmeasured |= any(
|
|
145
150
|
o is not None and o.price is not None and not o.price_measured for o in opts
|
|
146
151
|
)
|
|
147
|
-
|
|
152
|
+
stepped |= any(o is not None and o.stepped_down for o in opts)
|
|
153
|
+
notes = [f"Ranked for {ranked_for}."] if ranked_for else []
|
|
154
|
+
if runs:
|
|
155
|
+
notes.append(f"{RUNS_MARK} runs by default.")
|
|
156
|
+
if stepped:
|
|
157
|
+
notes.append(
|
|
158
|
+
f"{STEP_MARK} the Pick one level lower: no other maker is funded, and a "
|
|
159
|
+
"lower level uses less of a tight pool."
|
|
160
|
+
)
|
|
148
161
|
if priced:
|
|
149
162
|
notes.append(
|
|
150
163
|
"Price: list $ per 1M tokens (3 in : 1 out) × the tokens the model "
|
|
@@ -420,3 +433,143 @@ def relayout(
|
|
|
420
433
|
converted += 1
|
|
421
434
|
i = end
|
|
422
435
|
return "\n".join(out), converted, kept
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# --------------------------------------------------------------------------- #
|
|
439
|
+
# Re-ranking the tables this release writes
|
|
440
|
+
# --------------------------------------------------------------------------- #
|
|
441
|
+
|
|
442
|
+
_TABLE_RE: Final = re.compile(r"^\|\s*\|\s*Cost\b")
|
|
443
|
+
_RANKED_FOR_RE: Final = re.compile(
|
|
444
|
+
r"Ranked for ([a-z-]+), (low|medium|high) complexity(, novel)?\."
|
|
445
|
+
)
|
|
446
|
+
_SECTION_RE: Final = re.compile(r"^(?:## (Step \S+)|### (Phase [0-9A-Za-z.]+)|### (\S+))")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@dataclass(frozen=True)
|
|
450
|
+
class TableLadder:
|
|
451
|
+
"""A ladder table (0.2.95 on) in a roadmap, with the lines it spans: the
|
|
452
|
+
table, its note, its Conversation line and its Runs paragraph."""
|
|
453
|
+
|
|
454
|
+
start: int
|
|
455
|
+
end: int
|
|
456
|
+
section: str
|
|
457
|
+
phase: bool
|
|
458
|
+
done: bool
|
|
459
|
+
picks: tuple[tuple[str, str], ...]
|
|
460
|
+
ranks: tuple[tuple[str, ...], ...]
|
|
461
|
+
ranked_for: tuple[str, str, bool] | None
|
|
462
|
+
conversation: str | None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _skip_blank(lines: list[str], i: int) -> int:
|
|
466
|
+
while i < len(lines) and not lines[i].strip():
|
|
467
|
+
i += 1
|
|
468
|
+
return i
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _block_end(lines: list[str], i: int) -> int:
|
|
472
|
+
"""The index past a ladder's note paragraph, Conversation line and Runs
|
|
473
|
+
paragraph (each optional, in that order), from just past its table."""
|
|
474
|
+
k = _skip_blank(lines, i)
|
|
475
|
+
if k < len(lines) and lines[k].startswith(
|
|
476
|
+
("Ranked for ", f"{RUNS_MARK} ", "Price: ", f"{STEP_MARK} ")
|
|
477
|
+
):
|
|
478
|
+
while k < len(lines) and lines[k].strip():
|
|
479
|
+
k += 1
|
|
480
|
+
i = k
|
|
481
|
+
k = _skip_blank(lines, i)
|
|
482
|
+
if k < len(lines) and lines[k].startswith("**Conversation:**"):
|
|
483
|
+
i = k + 1
|
|
484
|
+
k = _skip_blank(lines, i)
|
|
485
|
+
if k < len(lines) and lines[k].startswith("**Runs:**"):
|
|
486
|
+
while k < len(lines) and lines[k].strip():
|
|
487
|
+
k += 1
|
|
488
|
+
i = k
|
|
489
|
+
return i
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def table_ladders(
|
|
493
|
+
text: str, *, phase_written: Callable[[str], bool] | None = None
|
|
494
|
+
) -> list[TableLadder]:
|
|
495
|
+
"""Every ladder table in ``text``, each with its section (`Step 5`,
|
|
496
|
+
`Phase 32`), whether it sits under a phase heading, whether its step or
|
|
497
|
+
phase is done (Complete, In progress, or a written phase), its Picks and
|
|
498
|
+
every rank's models, the classification its note names, and its
|
|
499
|
+
Conversation value."""
|
|
500
|
+
lines = text.split("\n")
|
|
501
|
+
out: list[TableLadder] = []
|
|
502
|
+
section, phase, done = "", False, False
|
|
503
|
+
i = 0
|
|
504
|
+
while i < len(lines):
|
|
505
|
+
line = lines[i]
|
|
506
|
+
if m := _SECTION_RE.match(line):
|
|
507
|
+
section = m.group(1) or m.group(2) or m.group(3)
|
|
508
|
+
phase = m.group(2) is not None
|
|
509
|
+
done = "✅" in line or bool(
|
|
510
|
+
phase and phase_written and phase_written(section.split(" ", 1)[1])
|
|
511
|
+
)
|
|
512
|
+
elif re.match(r"^#{1,2} ", line):
|
|
513
|
+
section, phase, done = "", False, False
|
|
514
|
+
elif status := _STATUS_RE.match(line):
|
|
515
|
+
done = done or status.group(1).startswith(("Complete", "In progress"))
|
|
516
|
+
elif line.startswith("**Phase roadmap settings**"):
|
|
517
|
+
# The session that writes a phase roadmap, under any heading
|
|
518
|
+
# (pyfinlab's `### F1 —` workstreams): a phase ladder.
|
|
519
|
+
phase = True
|
|
520
|
+
if not (_TABLE_RE.match(line) and i + 1 < len(lines) and _RULE_RE.match(lines[i + 1])):
|
|
521
|
+
i += 1
|
|
522
|
+
continue
|
|
523
|
+
j = i + 2
|
|
524
|
+
cells: dict[str, dict[str, list[str]]] = {}
|
|
525
|
+
rank = ""
|
|
526
|
+
while j < len(lines) and lines[j].startswith("|"):
|
|
527
|
+
label, *values = _table_cells(lines[j])
|
|
528
|
+
name = label.replace(" ", "").strip()
|
|
529
|
+
if name.startswith("**"):
|
|
530
|
+
rank = name.strip("*")
|
|
531
|
+
cells[rank] = {}
|
|
532
|
+
elif rank:
|
|
533
|
+
cells[rank][name] = [v.strip("*").replace(f" {STEP_MARK}", "") for v in values]
|
|
534
|
+
j += 1
|
|
535
|
+
end = _block_end(lines, j)
|
|
536
|
+
block = "\n".join(lines[j:end])
|
|
537
|
+
found = _RANKED_FOR_RE.search(" ".join(block.split()))
|
|
538
|
+
conv = next((m.group(1) for ln in lines[j:end] if (m := _CONVERSATION_RE.match(ln))), None)
|
|
539
|
+
pick = cells.get("Pick", {})
|
|
540
|
+
picks = tuple(zip(pick.get("Model", []), pick.get("Level", []), strict=False))
|
|
541
|
+
ranks = tuple(tuple(cells[r].get("Model", [])) for r in cells)
|
|
542
|
+
out.append(
|
|
543
|
+
TableLadder(
|
|
544
|
+
start=i,
|
|
545
|
+
end=end,
|
|
546
|
+
section=section,
|
|
547
|
+
phase=phase,
|
|
548
|
+
done=done,
|
|
549
|
+
picks=picks,
|
|
550
|
+
ranks=ranks,
|
|
551
|
+
ranked_for=(
|
|
552
|
+
(found.group(1), found.group(2), bool(found.group(3))) if found else None
|
|
553
|
+
),
|
|
554
|
+
conversation=conv,
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
i = end
|
|
558
|
+
return out
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def replace_ladders(text: str, replacements: Mapping[int, tuple[int, str]]) -> str:
|
|
562
|
+
"""``text`` with each ladder starting at line ``start`` (a key) replaced,
|
|
563
|
+
through its ``end`` line, by the given Markdown."""
|
|
564
|
+
lines = text.split("\n")
|
|
565
|
+
out: list[str] = []
|
|
566
|
+
i = 0
|
|
567
|
+
while i < len(lines):
|
|
568
|
+
if i in replacements:
|
|
569
|
+
end, md = replacements[i]
|
|
570
|
+
out.extend(md.split("\n"))
|
|
571
|
+
i = end
|
|
572
|
+
continue
|
|
573
|
+
out.append(lines[i])
|
|
574
|
+
i += 1
|
|
575
|
+
return "\n".join(out)
|
|
@@ -1981,7 +1981,7 @@ def _backup_for(
|
|
|
1981
1981
|
) -> Backup | None:
|
|
1982
1982
|
"""The rung's backup: the substitute from another maker that does the job
|
|
1983
1983
|
as well as the rung, for the least. ``avoid`` leaves out further
|
|
1984
|
-
candidates
|
|
1984
|
+
candidates.
|
|
1985
1985
|
|
|
1986
1986
|
Over the pool's models from a maker other than the rung model's, each at
|
|
1987
1987
|
its measured efforts, draw the frontier and keep its adequate points,
|
|
@@ -2304,8 +2304,9 @@ RUN_COLUMN: Final[dict[str, str]] = {"cheap": "cost", "balanced": "balanced", "b
|
|
|
2304
2304
|
# The rule a step's runner applies to the pools on the day (roadmap-step §3).
|
|
2305
2305
|
RUN_RULE: Final[str] = (
|
|
2306
2306
|
"If that column's usage pool reads `tight` or `exhausted`, run the first of its "
|
|
2307
|
-
"Runner-up and Backup whose pool has headroom;
|
|
2308
|
-
"the
|
|
2307
|
+
"Runner-up and Backup whose pool has headroom; on a `tight` pool an option marked ↓ "
|
|
2308
|
+
"(the Pick one level lower) also runs. With none, a `tight` column runs the Cost "
|
|
2309
|
+
"column and an `exhausted` one its Backup."
|
|
2309
2310
|
)
|
|
2310
2311
|
# Dial levels a roadmap never pins: Codex's `ultra` and Claude Code's
|
|
2311
2312
|
# Ultracode add parallel agents one planning document does not need.
|
|
@@ -2387,10 +2388,17 @@ def ladder_price_by_name(
|
|
|
2387
2388
|
|
|
2388
2389
|
|
|
2389
2390
|
def _option(
|
|
2390
|
-
c: Candidate,
|
|
2391
|
+
c: Candidate,
|
|
2392
|
+
level: str,
|
|
2393
|
+
bench: dict[str, Any],
|
|
2394
|
+
catalog: dict[str, Any],
|
|
2395
|
+
*,
|
|
2396
|
+
stepped_down: bool = False,
|
|
2391
2397
|
) -> dict[str, Any]:
|
|
2392
2398
|
"""One ranked option of a ladder column: ``c`` run at ``level``, with its
|
|
2393
|
-
dial and level, its Thinking toggle, its AA Index and its price there
|
|
2399
|
+
dial and level, its Thinking toggle, its AA Index and its price there;
|
|
2400
|
+
``stepped_down`` when it is the Pick one or two levels lower, filling a
|
|
2401
|
+
rank no other maker could."""
|
|
2394
2402
|
dial = _DIAL_NAMES.get(c.platform_id, "Effort")
|
|
2395
2403
|
price = ladder_price(c.model_id, level, catalog)
|
|
2396
2404
|
return {
|
|
@@ -2412,6 +2420,7 @@ def _option(
|
|
|
2412
2420
|
"price_usd_per_1m": None if price is None else price[0],
|
|
2413
2421
|
"price_measured": None if price is None else price[1],
|
|
2414
2422
|
"thinking_cell": _thinking_cell(c, catalog),
|
|
2423
|
+
"stepped_down": stepped_down,
|
|
2415
2424
|
}
|
|
2416
2425
|
|
|
2417
2426
|
|
|
@@ -2426,57 +2435,22 @@ def _layout_option(o: dict[str, Any] | None) -> _layout.Option | None:
|
|
|
2426
2435
|
aa=_aa_cell(o["aa_index"]),
|
|
2427
2436
|
price=o["price_usd_per_1m"],
|
|
2428
2437
|
price_measured=bool(o["price_measured"]),
|
|
2438
|
+
stepped_down=bool(o.get("stepped_down")),
|
|
2429
2439
|
)
|
|
2430
2440
|
|
|
2431
2441
|
|
|
2432
|
-
def
|
|
2433
|
-
"""``
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
task: Task,
|
|
2445
|
-
headroom: str,
|
|
2446
|
-
bench: dict[str, Any],
|
|
2447
|
-
catalog: dict[str, Any],
|
|
2448
|
-
aa_band: float,
|
|
2449
|
-
) -> Backup | None:
|
|
2450
|
-
"""A Settings column's Backup, ranked under its pick and runner-up
|
|
2451
|
-
(``second``): the pick's backup (:func:`_backup_for`, another maker's)
|
|
2452
|
-
that is not the runner-up's series, from a maker neither row above uses
|
|
2453
|
-
where one meets the task's bar, so the three rows span as many makers as
|
|
2454
|
-
the pool holds. A runner-up from the pick's own maker leaves the pick's
|
|
2455
|
-
backup as it is. None when the runner-up holds the only other maker's
|
|
2456
|
-
series: it already stands in for an outage or an exhausted pool."""
|
|
2457
|
-
if second is None or second.candidate.provider == rung.candidate.provider:
|
|
2458
|
-
return rung.backup
|
|
2459
|
-
taken = second.candidate
|
|
2460
|
-
own = frozenset(_series_ids(taken.model_id, catalog))
|
|
2461
|
-
|
|
2462
|
-
def same_series(c: Candidate) -> bool:
|
|
2463
|
-
return c.model_id in own
|
|
2464
|
-
|
|
2465
|
-
third = _backup_for(
|
|
2466
|
-
rung,
|
|
2467
|
-
pool,
|
|
2468
|
-
task,
|
|
2469
|
-
headroom,
|
|
2470
|
-
bench,
|
|
2471
|
-
catalog,
|
|
2472
|
-
aa_band,
|
|
2473
|
-
avoid=lambda c: c.provider == taken.provider or same_series(c),
|
|
2474
|
-
)
|
|
2475
|
-
if third is not None and third.candidate.requirement_penalty == 0:
|
|
2476
|
-
return third
|
|
2477
|
-
return (
|
|
2478
|
-
_backup_for(rung, pool, task, headroom, bench, catalog, aa_band, avoid=same_series) or third
|
|
2479
|
-
)
|
|
2442
|
+
def _step_down(c: Candidate, level: str, steps: int, catalog: dict[str, Any]) -> str | None:
|
|
2443
|
+
"""``level`` on ``c``'s own dial, ``steps`` rungs lower (High → Medium);
|
|
2444
|
+
None below the dial's lowest rung, or on a surface with no dial."""
|
|
2445
|
+
if c.platform_id in _DIAL_NAMES and _DIAL_NAMES[c.platform_id] is None:
|
|
2446
|
+
return None
|
|
2447
|
+
levels = native_levels(c.platform_id, c.model_id, catalog=catalog) or list(EFFORT_LADDER)
|
|
2448
|
+
levels = [lv for lv in levels if normalize_level(lv) not in _NEVER_PINNED]
|
|
2449
|
+
names = [normalize_level(lv) for lv in levels]
|
|
2450
|
+
here = normalize_level(level)
|
|
2451
|
+
if here not in names or names.index(here) < steps:
|
|
2452
|
+
return None
|
|
2453
|
+
return levels[names.index(here) - steps]
|
|
2480
2454
|
|
|
2481
2455
|
|
|
2482
2456
|
def _deepest_level(c: Candidate, catalog: dict[str, Any]) -> str:
|
|
@@ -2503,21 +2477,27 @@ def settings_ladder(
|
|
|
2503
2477
|
day instead of the roadmap being refreshed. ``phase`` is a phase-roadmap
|
|
2504
2478
|
session: it runs Quality, at the deepest rung its dial allows.
|
|
2505
2479
|
|
|
2506
|
-
Each column ranks three options, each with its effort
|
|
2507
|
-
Intelligence Index
|
|
2480
|
+
Each column ranks three options from three makers, each with its effort,
|
|
2481
|
+
its AA Intelligence Index (:func:`ladder_aa_index`) and its price there.
|
|
2482
|
+
A tight pool is a maker's (for this operator a maker is a subscription),
|
|
2483
|
+
so a fallback from the Pick's own maker would draw on the pool that just
|
|
2484
|
+
ran short (2026-10-25: Quality ranked Opus 5.5, then Fable 5.1):
|
|
2508
2485
|
|
|
2509
2486
|
- Pick: the column's rung (:func:`ladder`).
|
|
2510
|
-
- Runner-up: the column's rung with the
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
AA Index
|
|
2487
|
+
- Runner-up: the column's rung with every model of the Pick's maker off
|
|
2488
|
+
the table.
|
|
2489
|
+
- Backup: the column's rung with both makers above off the table.
|
|
2490
|
+
|
|
2491
|
+
Where the pool holds too few makers, a missing rank is the Pick's own
|
|
2492
|
+
model one level lower on its dial (``stepped_down``; ↓ in the table),
|
|
2493
|
+
which uses less of a tight pool; with one maker, the Runner-up and Backup
|
|
2494
|
+
step down one and two levels. None below the dial's lowest level.
|
|
2495
|
+
|
|
2496
|
+
The ranks follow the column's own rule, not the AA Index: Cost ranks each
|
|
2497
|
+
maker's cheapest adequate option, so its AA Index may rise down the
|
|
2498
|
+
column. Returns the columns and the ladder table's Markdown
|
|
2499
|
+
(:mod:`roadmodel.ladder_layout`); None when no pool model carries an AA
|
|
2500
|
+
Index."""
|
|
2521
2501
|
cat = catalog if catalog is not None else _cost._load_catalog()
|
|
2522
2502
|
bench = benchmarks if benchmarks is not None else _load_benchmarks()
|
|
2523
2503
|
off = list(unavailable_models or [])
|
|
@@ -2539,8 +2519,14 @@ def settings_ladder(
|
|
|
2539
2519
|
lad = read([])
|
|
2540
2520
|
if lad is None:
|
|
2541
2521
|
return None
|
|
2542
|
-
|
|
2543
|
-
|
|
2522
|
+
# Every catalog model, by its maker: a rank leaves out the makers above
|
|
2523
|
+
# it. The whole catalog, not the pool: with GPT-6.1 Sol off the table the
|
|
2524
|
+
# pool takes GPT-6 Sol back, the same maker.
|
|
2525
|
+
by_maker: dict[str, set[str]] = {}
|
|
2526
|
+
for m in cat.get("models", []):
|
|
2527
|
+
mid = str(m.get("id", ""))
|
|
2528
|
+
maker = _cost.model_provider(mid, cat) or str(m.get("provider", "unknown"))
|
|
2529
|
+
by_maker.setdefault(maker, set()).add(mid)
|
|
2544
2530
|
|
|
2545
2531
|
def level_of(tier: str, r: Rung) -> str:
|
|
2546
2532
|
return _deepest_level(r.candidate, cat) if phase and tier == "quality" else r.level
|
|
@@ -2548,20 +2534,27 @@ def settings_ladder(
|
|
|
2548
2534
|
columns: dict[str, dict[str, Any]] = {}
|
|
2549
2535
|
for tier in LADDER_TIERS:
|
|
2550
2536
|
rung = lad.rungs[tier]
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2537
|
+
pick_level = level_of(tier, rung)
|
|
2538
|
+
pick = _option(rung.candidate, pick_level, bench, cat)
|
|
2539
|
+
ranked: list[dict[str, Any] | None] = [pick]
|
|
2540
|
+
makers = [rung.candidate.provider]
|
|
2541
|
+
while len(ranked) < 3:
|
|
2542
|
+
rest = read(sorted(m for maker in makers for m in by_maker.get(maker, ())))
|
|
2543
|
+
nxt = None if rest is None else rest.rungs[tier]
|
|
2544
|
+
if nxt is None or nxt.candidate.provider in makers:
|
|
2545
|
+
break
|
|
2546
|
+
ranked.append(_option(nxt.candidate, level_of(tier, nxt), bench, cat))
|
|
2547
|
+
makers.append(nxt.candidate.provider)
|
|
2548
|
+
steps = 1
|
|
2549
|
+
while len(ranked) < 3:
|
|
2550
|
+
lower = _step_down(rung.candidate, pick_level, steps, cat)
|
|
2551
|
+
ranked.append(
|
|
2557
2552
|
None
|
|
2558
|
-
if
|
|
2559
|
-
else _option(
|
|
2560
|
-
)
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
),
|
|
2564
|
-
}
|
|
2553
|
+
if lower is None
|
|
2554
|
+
else _option(rung.candidate, lower, bench, cat, stepped_down=True)
|
|
2555
|
+
)
|
|
2556
|
+
steps += 1
|
|
2557
|
+
columns[tier] = {**pick, "runner_up": ranked[1], "backup": ranked[2]}
|
|
2565
2558
|
runs = "quality" if phase else RUN_COLUMN.get(task.budget, "balanced")
|
|
2566
2559
|
lists = _layout.ladder_markdown(
|
|
2567
2560
|
{
|
|
@@ -2574,6 +2567,10 @@ def settings_ladder(
|
|
|
2574
2567
|
},
|
|
2575
2568
|
runs=runs,
|
|
2576
2569
|
conversation="New",
|
|
2570
|
+
ranked_for=(
|
|
2571
|
+
f"{task.category}, {task.complexity} complexity"
|
|
2572
|
+
+ (", novel" if task.novel and task.complexity == "high" else "")
|
|
2573
|
+
),
|
|
2577
2574
|
)
|
|
2578
2575
|
why = (
|
|
2579
2576
|
"a phase-roadmap session runs Quality (quality first)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|