Skip to content

Commit ec42b83

Browse files
Viktor Sovietovclaude
andcommitted
v0.16: context economics, an injection gate, and size discipline
Three methods reimplemented from virgiliojr94/book-to-skill. `okfy cost` — tokens entering context to answer ONE question three ways: corpus dump (RECURRING, re-billed every turn), naive navigation (a MODEL with its assumptions printed beside it), and bundle retrieval (measured: the real query() over the bundle's real test_queries). Every line is tagged measured or modelled. A report, never a gate: exits 0 whatever the numbers say. Injection scanning — concept bodies, frontmatter and the generated docs are scanned for instruction override, role injection, chat-template tags, identity reassignment, exfiltration and invisible codepoints. Warnings by default, errors under `okfy validate --strict-injection`, and E_REL_INJECTION in release-check. The declared hatch acceptance.allow_injection downgrades phrase findings to a note that still names the count, and never excuses an invisible codepoint. The phrase rules are phrase-keyed and are not topic-complete; only the unicode rule generalises. `okfy budget` — per-type size distribution against optional archetype targets, and the ALWAYS-RESIDENT total (AGENTS.md + index.md) reported separately because it is the only cost billed on every turn. Advisory at every strictness level. Depth is earned with content, not with a bigger number: thin concepts are reported, never padded. Measured rather than asserted: swapping the token counter moves the absolute figures ~380% and the ratios between strategies 8-15%, so the report leads with the ratio and calls it steadier rather than stable. core 525 passed + 2 xfailed, MCP 41 passed, ruff clean in both packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 14aa906 commit ec42b83

21 files changed

Lines changed: 1151 additions & 12 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Large diffs are not rendered by default.

adapters/mcp/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "okfy-mcp"
3-
version = "0.15.0"
3+
version = "0.16.0"
44
description = "MCP stdio adapter for OKFy knowledge bundles"
55
license = "Apache-2.0"
66
requires-python = ">=3.11"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.15.0"
1+
__version__ = "0.16.0"

core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "okfy"
3-
version = "0.15.0"
3+
version = "0.16.0"
44
description = "Purpose-shaped OKF knowledge bundles: extraction at scale, deterministic retrieval"
55
license = "Apache-2.0"
66
requires-python = ">=3.11"

core/src/okfy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.15.0"
1+
__version__ = "0.16.0"

core/src/okfy/archetype.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class Archetype:
2020
# {type: {field: [allowed values]}} — closed vocabularies the validator
2121
# enforces (E_FIELD_ENUM); free text in an enum field is machine-invisible
2222
field_enums: dict[str, dict[str, list[str]]]
23+
# OPTIONAL token-size guidance: {"types": {T: {target_min, target_max}},
24+
# "resident_max": int}. Advisory only — `okfy budget` reports against it and
25+
# nothing gates on it. An archetype without the block is not defective; its
26+
# report simply shows no targets.
27+
budgets: dict
2328
consumption_protocol: str
2429
root: Path
2530

@@ -44,6 +49,7 @@ def load_archetype(name: str) -> Archetype:
4449
link_rules=d.get("link_rules", []),
4550
nonempty_sections=d.get("nonempty_sections", {}),
4651
field_enums=d.get("field_enums", {}),
52+
budgets=d.get("budgets", {}) or {},
4753
consumption_protocol=d.get("consumption_protocol", ""),
4854
root=root,
4955
)

core/src/okfy/archetypes/codebase-map/archetype.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,17 @@ purpose_checks:
3535
For Module/Convention concepts: does it state what a change must preserve
3636
(interfaces, invariants, conventions) concretely enough that an agent
3737
editing the code would know the rules? Vague guidance is a failure.
38+
# OPTIONAL and ADVISORY, derived the same way as decision-support's, from the
39+
# measured distribution of the real codebase-map bundle (298 concepts).
40+
# Depth is earned with content, not with a bigger number: a thin concept should
41+
# be reported thin, never padded to reach the floor.
42+
budgets:
43+
types:
44+
Convention: {target_min: 300, target_max: 900}
45+
DataModel: {target_min: 350, target_max: 950}
46+
Decision: {target_min: 300, target_max: 850}
47+
Flow: {target_min: 350, target_max: 850}
48+
Module: {target_min: 400, target_max: 1300}
49+
resident_max: 12000
50+
3851
consumption_protocol: protocols/agents.tmpl

core/src/okfy/archetypes/decision-support/archetype.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,23 @@ purpose_checks:
3232
For Strategy/Playbook concepts: are entry conditions, risks, and exit
3333
criteria concrete enough to act on? Vague advice ("manage risk carefully")
3434
is a failure.
35+
# OPTIONAL and ADVISORY. Derived from the MEASURED distribution of the three
36+
# real decision-support bundles (656 concepts): target_min sits near the
37+
# observed floor, target_max near the observed p90. `okfy budget` reports
38+
# against these; nothing gates on them.
39+
#
40+
# Depth is earned with content, not with a bigger number. A concept that
41+
# genuinely has less to say should land below target_min and be reported thin —
42+
# padding it to reach the floor is the wrong fix and makes the bundle worse.
43+
budgets:
44+
types:
45+
GlossaryTerm: {target_min: 120, target_max: 400}
46+
MarketRegime: {target_min: 300, target_max: 1200}
47+
Playbook: {target_min: 250, target_max: 900}
48+
RiskFactor: {target_min: 250, target_max: 1000}
49+
Strategy: {target_min: 800, target_max: 2100}
50+
# AGENTS.md + index.md are read on EVERY turn; concepts are on demand. This
51+
# is the only figure billed repeatedly, which is why it has its own target.
52+
resident_max: 12000
53+
3554
consumption_protocol: protocols/agents.tmpl

core/src/okfy/budget.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Size discipline for concepts and for the files billed on every turn.
2+
3+
The transferable rule from `virgiliojr94/book-to-skill` is not its numbers, it is
4+
the sentence beside them: depth is earned with content, not with a bigger number.
5+
A chapter that genuinely has less to say should land below the floor and be
6+
reported thin. Padding it to hit a target makes the artifact worse while making
7+
the metric better, so this report says so in its own output — a number printed
8+
without that sentence invites exactly the wrong fix.
9+
10+
OKFy required sections and never sizes. More to the point, nothing in the tool
11+
distinguished the two economies it actually has: `AGENTS.md` and `index.md` are
12+
resident and re-read on EVERY turn, while concepts are fetched on demand. Those
13+
are different costs and this module reports them separately.
14+
15+
Advisory by the owner's decision. `okfy budget` exits 0 always,
16+
`W_BUDGET_RESIDENT` is a warning at every strictness level, and `release_check`
17+
never composes any of it.
18+
"""
19+
from okfy.bundle import Bundle
20+
from okfy.tokens import count_path, token_method
21+
22+
RESIDENT_FILES = ("AGENTS.md", "index.md")
23+
24+
ANTI_PADDING = (
25+
"depth is earned with content, not with a bigger number: a thin concept "
26+
"should stay thin and be reported, never padded to reach a target"
27+
)
28+
29+
30+
def resident_core(bundle: Bundle) -> dict:
31+
"""The files an agent keeps loaded for a whole session.
32+
33+
One predicate, two callers: `okfy cost` bills this into its retrieval
34+
strategy and `okfy budget` reports it against a target. Two modules with
35+
two definitions of "resident" is the drift shape the audits kept finding.
36+
"""
37+
tokens, present, missing = 0, [], []
38+
for name in RESIDENT_FILES:
39+
p = bundle.root / name
40+
if p.is_file():
41+
tokens += count_path(p)
42+
present.append(name)
43+
else:
44+
missing.append(name)
45+
return {"tokens": tokens, "files": present, "missing": missing}
46+
47+
48+
def _percentile(sorted_vals: list[int], q: float) -> int:
49+
if not sorted_vals:
50+
return 0
51+
i = min(len(sorted_vals) - 1, int(round(q * (len(sorted_vals) - 1))))
52+
return sorted_vals[i]
53+
54+
55+
def _median(sorted_vals: list[int]) -> int:
56+
n = len(sorted_vals)
57+
if not n:
58+
return 0
59+
mid = n // 2
60+
return sorted_vals[mid] if n % 2 else (sorted_vals[mid - 1] + sorted_vals[mid]) // 2
61+
62+
63+
def budget_report(bundle: Bundle, archetype=None) -> dict:
64+
"""Read-only. An archetype with no `budgets:` block is not a defect: every
65+
target reads `None` and no warning is produced."""
66+
budgets = (getattr(archetype, "budgets", None) or {})
67+
type_targets = budgets.get("types") or {}
68+
resident_max = budgets.get("resident_max")
69+
70+
sizes: dict[str, list[int]] = {}
71+
ids: dict[str, list[tuple[str, int]]] = {}
72+
unreadable: list[dict] = []
73+
for c in bundle.concepts():
74+
if c.id.startswith("meta/"):
75+
continue
76+
t = str(c.meta.get("type"))
77+
try:
78+
n = count_path(c.path)
79+
except OSError as e:
80+
unreadable.append({"id": c.id, "reason": type(e).__name__})
81+
continue
82+
sizes.setdefault(t, []).append(n)
83+
ids.setdefault(t, []).append((c.id, n))
84+
85+
types = []
86+
for t in sorted(sizes):
87+
v = sorted(sizes[t])
88+
target = type_targets.get(t) or {}
89+
tmin, tmax = target.get("target_min"), target.get("target_max")
90+
thin = sorted(i for i, n in ids[t] if tmin is not None and n < tmin)
91+
over = sorted(i for i, n in ids[t] if tmax is not None and n > tmax)
92+
types.append({"type": t, "count": len(v), "median": _median(v),
93+
"p90": _percentile(v, 0.9), "min": v[0], "max": v[-1],
94+
"target_min": tmin, "target_max": tmax,
95+
"thin": thin, "over": over})
96+
97+
resident = resident_core(bundle)
98+
resident["target_max"] = resident_max
99+
resident["over"] = bool(resident_max and resident["tokens"] > resident_max)
100+
101+
notes = [ANTI_PADDING]
102+
if not budgets:
103+
notes.append(
104+
f"archetype {getattr(archetype, 'name', '(none)')!r} declares no "
105+
"budgets: block — targets read '-' and nothing is out of range. "
106+
"The block is optional; its absence is not a defect")
107+
if resident["missing"]:
108+
notes.append("not packaged: " + ", ".join(resident["missing"])
109+
+ " absent, so the resident total is undercounted")
110+
if unreadable:
111+
notes.append(f"{len(unreadable)} concept(s) could not be read and are "
112+
"absent from every figure below")
113+
return {"bundle": str(bundle.root),
114+
"archetype": getattr(archetype, "name", None),
115+
"token_method": token_method(),
116+
"resident": resident, "types": types,
117+
"unreadable": unreadable, "notes": notes,
118+
"anti_padding": ANTI_PADDING}

core/src/okfy/cli.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44

55
from okfy.commands import HANDLERS
6+
from okfy.cost import DEFAULT_N as COST_N
67
from okfy.guard import GuardError
78
from okfy.segment import DEFAULT_BUDGET
89

@@ -51,6 +52,28 @@ def main(argv=None) -> int:
5152
p.add_argument("--quiet", action="store_true",
5253
help="summary only, no per-group detail")
5354

55+
p = sub.add_parser(
56+
"cost",
57+
help="read-only context-economics report: tokens entering context to "
58+
"answer one question three ways (not a gate, never fails a build)")
59+
p.add_argument("bundle", type=Path)
60+
p.add_argument("--query", default=None,
61+
help="one question to cost (default: purpose.md test_queries)")
62+
p.add_argument("-n", type=int, default=COST_N,
63+
help="results per query for the retrieval strategy")
64+
p.add_argument("--json", action="store_true")
65+
p.add_argument("--quiet", action="store_true", help="totals only")
66+
67+
p = sub.add_parser(
68+
"budget",
69+
help="advisory size report: concept sizes per type against the "
70+
"archetype's declared targets, and the always-resident total "
71+
"(never a gate, no strict flag, exits 0)")
72+
p.add_argument("bundle", type=Path)
73+
p.add_argument("--no-archetype", action="store_true")
74+
p.add_argument("--json", action="store_true")
75+
p.add_argument("--quiet", action="store_true", help="no thin-concept list")
76+
5477
p = sub.add_parser("validate"); p.add_argument("bundle", type=Path)
5578
p.add_argument("--all", action="store_true", help="include drafts and proposals")
5679
p.add_argument("--no-archetype", action="store_true")
@@ -72,6 +95,9 @@ def main(argv=None) -> int:
7295
help="every concept type must be declared — in "
7396
"meta/extraction-plan.md `types` if the archetype was "
7497
"adapted, else the archetype's canonical_types")
98+
p.add_argument("--strict-injection", action="store_true",
99+
help="corpus-borne instructions (injection scan) become "
100+
"errors instead of warnings — what release-check uses")
75101
p.add_argument("--quiet", action="store_true")
76102

77103
p = sub.add_parser(

0 commit comments

Comments
 (0)