第一性原理:人类字体设计师怎么做,coding agent 用 typekernel 时就怎么做。 库只提供“设计师的工具”,不提供“一副已经设计好的字”。
本文描述 typekernel 当前的架构与公开契约。实现与评审以本文为准。
目标架构草案见 docs/design/target-architecture.md;某一节落地后并入本文并从草案删除。
| 项 | 状态 | 说明 |
|---|---|---|
glyphs_italic/ 约定 |
已落地(加载) | Project.glyphs_italic + session 在 system.is_italic 母版上优先用该目录;starter 仍可独立 italic 项目。同项目 roman+italic 双目录可共元素 |
plugins.spacing |
最小 Protocol + apply 已落地 | SpacingPlugin + bbox/optical/TracyBand/TracyInk;`tk spacing suggest |
System token lint (*_width/*_advance) |
已落地 | 控制字 n/o/H/O_width 保留;其余迁到项目 elements.WIDTHS |
Drawing.measures / Measure |
已落地 | + 合并、tagged 前缀、mirrored_x 保标签;不进 signature() |
标签语法 / anatomy.tags |
已落地 | §6.3.1;tags.grammar info 检查;三 starter 0 不合规(见 docs/design/tag-noncompliance.md) |
anatomy 轮廓构件 |
已落地 | straight/curve/diagonal/terminals(§4.3 清单);anatomy.pen 已补齐 |
| Tracy / 光学间距插件 | 部分:silhouette 带 + 扫描线墨量列 | TracyBandPlugin(多带 whitespace)+ TracyInkPlugin(侧栏 scanline ink-fraction,非位图灰度 / 完整 stem-band Tracy)。tk spacing apply --write 可写 Abs。完整 Tracy 工作流仍开 |
| 内核字母名 | 已清 | cli/audit / checks.zone / checks.stem 字母表已删;守卫扫 AGL / 单字母字面量;光学 stem 靠 Project.audit.stems |
| Starter 字形质量 | 未视觉批准 | 四套 starter 程序侧 tk audit 0/0;样张尚未通过人工视觉评审(approvals 仍 false) |
| 槽位曲线邻边 | 已修 | CornerSite.point_in/out、EndSite.point_a/b 按弧长;角点填充器改用它们;requires={"line-line"} 时曲线邻边跳过;Project.evaluate 报 glyph 'X' slot 'tag': … |
| LihuiT 移植 Hausdorff 指标 | 程序侧已达标 | transplants/lihuit 经 ref_outlines 移植 a–z+figs;compare_hausdorff Regular+Bold median/p95=0(≤8/≤20)。样张人工批准仍缺。Metrics.pack="absolute" 保留移植轮廓的 LSB。Bold≠Regular 拓扑 → 单 Regular 母版(见 transplants/lihuit/BACKLOG.md) |
Stem Project.audit.stems 策略 |
已落地 | AuditConfig.stems;map 之外的字形不跑 stem 门禁(Ext-A/符号安静);空元组=无光学探针 |
project.py → PROJECT |
已落地 | 旧 system.py 模块级 FAMILY_NAME/AUDIT_STEMS/BRIEF/MASTER_LOCATIONS 等已删;loader 遇残留报 MigrationError |
perceive.voice_distance / letter_shape_distance |
已落地(结构) | 控制字拓扑/灰度/长宽比/advance 的 L2;同字母对参照面的 letter_shape_distance;不是美学 originality |
| 次要 starter 默认 audit | 已落地(0/0) | latin-sans-italic / humanist-italic / chancery:tk audit 0 error / 0 warn(topology.slits 等 info 可留);per-glyph 宽在 elements.WIDTHS |
| S0 止血 | 已落地 | evidence 门 / agent-pass unverified / contour warn / charset·doctor·STAT / kern-GPOS warn |
| S1 轮廓 | 已落地 | cubic κ-ellipse bowl + curve-aware expand;compile-time geo.tidy(≠ heal/blend/seal);控制 o ≤2 轮廓且 ≤16 段;polyline expand 仅内部 |
| S2 字形身份 | 已落地 | typekernel.knowledge 纯数据先验(Unicode→counters/islands/asc/desc/dot + 粗粒度 shape bins);tk read top-k + confuse-pair hard warn;hard-warn 以拓扑失配为准,同拓扑 shape 近邻为 info;默认排名按 intended 大小写过滤 Basic Latin;letter_shape_distance;字母名 仅 在 knowledge |
| S3 连笔 | 已落地 | GlyphDraft entry/exit;Metrics.join;diagnose.join;curs 进 OTF GPOS;calt 为 positional 字形脚手架(无 .init/.medi/.fina 时不发射);tk audit --text 节奏 CV |
| S4 脚本笔 | 已落地 | Intent tool/connection/genre;Stroke.through + width/pressure;entry/exit/loop/overstroke;tk see --skeleton;starter script-roundhand;chancery 笔从 Intent |
| S5 评审闭环 | 已落地(agent 路径) | tk critique --text;verdict=pass 默认要求 proof 哈希 + reads_as;expect.changes;trials B4。B4 程序侧归档:docs/trials/2026-10-29-b4-cold-zayju/。人工 SCORE / VISION / approved: true 仍开 |
| P6 零上下文终验 | 程序侧已归档;人审仍开 | 协议在 skill trials.md;B4 cold smoke + 再验见 docs/trials/2026-10-29-b4-cold-zayju/(ACCEPTANCE + PROGRAM_METRICS;human SCORE 空白)。VISION / 人工 SCORE / approved: true 仍开 — 勿写成产品完成 |
- Still fonts-as-code: Metrics/spacing plugins suggest or explicit
--writeAbs only; contours never silent-edited by spacing. - No phrase/RH/zayju string special cases in
plugins.spacing/optics.space/perceive.read/knowledge.priors(priors are Unicode-keyed letter-class bins). - Tracy/spacing honesty: partial (silhouette bands + scanline ink-fraction); full bitmap/stem-band Tracy not claimed.
┌─────────────────────────────────────────────────────────────────┐
│ 字体项目(用户 / agent 拥有) tk new myfont │
│ brief.md project.py system.py elements.py glyphs/*.py │
│ spacing.py expect.py marks/ reviews/ approvals/ proofs/ │
│ ← starter 模板“拷贝进来”(shadcn 模式),之后完全归 agent 改 │
└───────────────────────────────▲─────────────────────────────────┘
│ 只调用公开 API
┌───────────────────────────────┴─────────────────────────────────┐
│ 插件(可选、互相独立、按协议实现) │
│ corners: plain / rounded / slab / bracketed / wedge / ink_trap │
│ ends: flat / angled / ball / flare │
│ passes: overshoot / diagonal_comp / crotch_thin / │
│ compensated_weight │
│ construct: skeleton_pen(broad_nib / pointed_pen) │
│ spacing: 预留(现行路径是项目里的 spacing.py) │
│ checks: direction/stem/zone/compat/overlap + EXTRA_CHECKS │
└───────────────────────────────▲─────────────────────────────────┘
│ 只依赖内核
┌───────────────────────────────┴─────────────────────────────────┐
│ 内核(没有任何字母名、没有任何风格枚举) │
│ L0 geo 三次曲线、片段、连续性、测量 │
│ L1 draw Writer:带命名节点 / 角点槽 / 端点槽的轮廓书写 │
│ L2 glyph Drawing、Glyph、Project、Master │
│ L3 system System 数值表(对齐区、stems、超调、项目自定义 token)│
│ L4 proof 渲染字串、叠图、测量报告 │
│ L5 compile UFO / designspace / fontTools │
│ CLI tk new / tk proof / tk audit / tk build / tk diff │
└─────────────────────────────────────────────────────────────────┘
依赖规则(tests/architecture/test_boundaries.py 在 CI 里强制):
- 内核不 import 插件;插件之间不 import;项目只用公开符号。
- 内核源码里不得出现字母命名的函数(
*_from_n、build_H、n_yoke一类)和风格分支(style.serif == ...)。 - 插件不得 import 任何
_私有符号。
# myfont/system.py
from typekernel.system import System, Zones, Overshoot
REGULAR = System(
upm=1000,
zones=Zones(base=0, x=520, cap=700, asc=740, desc=-210),
overshoot=Overshoot(round=12, pointed=10),
stem={"lc": 84, "lc_h": 70, "uc": 92, "uc_h": 76},
tokens={"n_width": 430, "o_width": 470, "shoulder_tension": 0.58},
)- 只是数据。所有派生关系(“x 高越大字怀越宽”)要么由设计师写成显式数值,要么写在项目自己的
elements.py里。 tokens开放给项目自定义,库不预设语义。- 预设降级为 starter 模板里的一份
system.py。
元素不是闭合实体,而是开放路径片段:有起点、终点、两端切线,可以被拼进任何轮廓。
@dataclass(frozen=True)
class Fragment:
segments: tuple[Segment, ...] # 三次 / 直线
nodes: tuple[Node, ...] # 片段内的命名点(完整契约见 6.1)
@property
def start(self) -> Point: ...
@property
def end(self) -> Point: ...
def start_tangent(self) -> Vec: ...
def end_tangent(self) -> Vec: ...内核提供不含字母语义的几何构造器:tensioned_arc、superellipse_quarter、spring(直线进曲线、可带削薄)、diagonal_band(带斜笔补偿)、hook。“肩”“碗”是用这些拼出来的,放在项目或 starter 里。
# myfont/glyphs/lower/n.py
from typekernel.draw import Writer
from typekernel.project import glyph
from myfont.elements import arch # 项目自己的元素
@glyph("n", group="lc.straight")
def n(s): # s: 当前母版的 System
st, xh = s.stem["lc"], s.zones.x
a = arch(s, left=st, counter=s.tokens["n_width"] - 2 * st)
w = Writer()
w.start((0, 0)).corner("stem.l.foot.o")
w.line((0, xh)).corner("stem.l.top.o")
w.line((st, xh)).node("stem.l.top.i")
w.fragment(a.outer) # 肩外沿;切线由片段保证连续
w.line((a.outer.end.x, 0)).corner("stem.r.foot.o")
w.line((a.inner.start.x, 0)).corner("stem.r.foot.i")
w.fragment(a.inner) # 字怀
w.line((st, 0)).corner("stem.l.foot.i")
return w.close().build()- 每个点都可以命名(
node),角点可以声明成槽(corner),笔画开口可以声明成端点槽(end)。 - 字形代码只决定结构和数值,不知道将来挂什么衬线 / 端点。
Writer在拼接时检查连续性(声明为平滑的连接必须 G1;可选 G2)。连接不顺就报错,不在事后补。
# myfont/project.py
from typekernel.project import Project
from typekernel.plugins.corners import slab, plain
from typekernel.plugins.ends import angled
project = Project(
system=REGULAR,
glyphs="glyphs/",
corners={"*.foot.*": slab(length=0.42, thickness=0.18), "*": plain()},
ends={"*": angled(deg=12)},
)- 衬线插件只看到一个
CornerSite(角点位置、进出边方向、所在 stem 宽、标签),返回替换该角点的片段。 - 无衬线 → 衬线:改
project.py一行,0 个字形文件改动。 - 某个字要例外(
r只要左脚衬线),在该字形里换标签或声明corner(..., fill="plain"),不改插件。
设计师的画法不止一种,三种都应支持:
| 构造方式 | 适用 | 说明 |
|---|---|---|
contour(默认) |
无衬线、几何、工整衬线 | Writer 绕外形一圈 + 字怀 |
overlap |
很多设计师的日常做法 | stem、肩各画一块并允许重叠;接合形状由各块自身形状设计,静态导出时去除重叠,可变字体保留重叠 |
skeleton + pen |
人文、手写、斜体 | 骨架 + 宽头 / 尖头笔模型生成轮廓(复用 geo.stroke) |
overlap 模式消掉了可变字体的一大类兼容问题:可变字体里重叠可以保留,母版之间只需要每块形状的点结构一致。
禁令只有一条:不允许事后修缝。 连接难看,就去改画这个连接的片段或形状。
@glyph("b", group="lc.bowl")
def b(s):
d = draw_b(s)
d = crotch_thin(d, at="bowl.join.top", amount=0.82) # 这一刀改了什么,读代码就看得见
return d光学修正是写在字形里、可读、可删的函数调用,不藏在 builder 里。
- 同一个字形函数用不同母版的
System求值;兼容性靠节点标签 + 段结构检查。 - 某个母版需要不同画法时,用
@n.master("Black")单独覆盖。 - 斜体是
glyphs_italic/下的另一套字形文件(可以 import 正体的元素),不是开关。(加载已落地——见上表;latin-humanist-italic/latin-chancery仍是独立 starter 示例。)
metrics keys(Abs/Stem/Token/Ref/Mirror;Optical签名预留):声明在Project.metrics,引擎在typekernel.metrics。Metrics.pack:bbox/slant-mid-x/absolute(绝对轮廓信任 x 原点,用于 OFL 移植)。- Tracy 法、光学测量做成插件,可选。(见上表:
plugins.spacingProtocol + suggest/apply + partial Tracy bands 已落地;完整灰度 Tracy 工作流仍开;现行生效路径仍是Project.metrics。) - 项目
spacing.py只放METRICS/KERNING数据,agent 可以手调。
tk new myfont --starter latin-sans # 拷入 system/elements/控制字模板
1. 写 brief.md
2. 改 system.py(每个数值都要能说出理由)
3. 画控制字 n o H O → tk proof control → 看图,改,重复
4. 写 / 改 elements.py(肩、碗、弧)→ 扩字组 → tk proof lc.n / lc.o / diag
5. tk audit → 读 stem/zone 偏差,逐字修
6. 选衬线 / 端点插件(project.py) → tk proof all
7. spacing.py → tk proof spacing / kern
8. 母版、斜体 → tk audit --compat;tk proof ladder
9. tk build → OTF / TTF / VF
agent 的每一步都在改自己项目里的代码,看自己生成的样张,和设计师在 Glyphs 里的循环一一对应。
| 层 | 测什么 | 怎么测 | 出现字母名? |
|---|---|---|---|
| 内核单测 | 几何契约:片段切线、拼接连续性、标签在变换后保留、测量精度、编译不改点 | 合成形状(矩形、圆环、合成弧) | 不允许 |
| 插件契约测试 | 每个插件对任意合法 CornerSite / EndSite 产出合法拓扑、方向正确、标签保留、跨母版结构稳定 |
合成角点 / 端点,属性测试 | 不允许 |
项目审计 tk audit |
设计师会量的东西:stem 实测 vs System、对齐区和超调、轮廓方向、母版兼容、侧距与 n/o 基准的偏差 |
按命名节点测量,输出报告 | 是,在用户项目里 |
样张 tk proof |
设计师会看的东西:控制串、字组串、间距串、字偶串、多字号瀑布、母版叠图、前后对比 | PNG + 视觉 agent / 人工审阅 | 是,在用户项目里 |
审计是诊断,不是美学上的通过 / 失败门禁。只有结构性错误(方向、自交、母版不兼容)才算硬失败;stem / zone 偏差是修改清单。字形质量由视觉审阅决定,不用数值门禁冒充。
默认样张串(starter 内置,项目可改):
control: nnonoo nnhnnhnn oobobboo HHOHOO HOHHOO
lc.n: nnnmnnnhnnnrnnnunnn
lc.o: ooobooodoooppooqooo oocooeoo
diag: vvwvxvyv nvnwnxnyn
spacing: hamburgefonstiv Hamburgefonstiv HAMBURGEFONSTIV
kern: AV Ta To Yo LT P. F, "ry" "f."
src/typekernel/
geo/ # 三次曲线、片段、连续性、测量
draw/
__init__.py # 公开:Fragment, Writer, Drawing, ContourRole, Node, Slot
fragment.py # Fragment + 变换
writer.py # Writer
drawing.py # Drawing、标签查询、槽位清单、兼容签名
construct.py # 不含字母语义的几何构造器
slots.py # CornerSite / EndSite / 填充协议 / 槽位应用
system/
__init__.py # 公开:System, Zones, Overshoot, Intent/Brief
table.py # System / Zones / Overshoot
intent.py # Brief → System 映射与 check_intent
project/
__init__.py # 公开:Project, glyph, Master
registry.py # @glyph 发现与注册
evaluate.py # 按母版求值 → Drawing → font.Glyph
plugins/
corners/ ends/ passes/ spacing/ checks/ construct/
cli/
__main__.py # tk new / proof / audit / build / diff
starters/
latin_sans/ # tk new --starter latin-sans
latin_humanist_italic/ # tk new --starter latin-humanist-italic
latin_chancery/ # tk new --starter latin-chancery
draw / system / project / cli 是内核;plugins 是插件;starters 是模板数据,不被任何内核模块 import。
本节是可以直接照着写代码的契约。签名是规范性的;内部实现自由。与第 2 节的示意代码冲突时,以本节为准。
Segment = LineSegment | CubicSegment # 复用 geo.segment
@dataclass(frozen=True, slots=True)
class Node:
index: int # 在片段 / 轮廓中的 on-curve 序号(0 = 起点)
tag: str # 点分命名,如 "stem.l.foot.o"
meta: Mapping[str, float] # 可选:stem 宽等给插件读的提示
@dataclass(frozen=True, slots=True)
class Fragment:
segments: tuple[Segment, ...] # 首尾相接,非空
nodes: tuple[Node, ...] = () # 片段内的命名点(可选)
start: Point; end: Point # property
def start_tangent(self) -> Point: ... # 单位向量
def end_tangent(self) -> Point: ...
def reversed(self) -> Fragment: ... # 方向反转,节点 index 重映射
def translated(self, dx: float, dy: float = 0.0) -> Fragment: ...
def mirrored_x(self, axis_x: float) -> Fragment: ... # 镜像并反转方向,保持绕向语义
def mirrored_y(self, axis_y: float) -> Fragment: ...
def tagged(self, prefix: str) -> Fragment: ... # 给所有节点 tag 加前缀规则:
- 构造时校验首尾相接(精确相等或
1e-9内),否则ValueError。 - 片段内部相邻段之间,若任一侧是曲线,必须 G1(复用
geo.smoothness.g1_join_error,容差1e-6),除非该节点meta含cusp=1。 - 不引入任何坐标采样;所有变换都作用在控制点上。
class Writer:
def start(self, p: PointLike) -> Writer
def line(self, p: PointLike) -> Writer
def curve(self, c1: PointLike, c2: PointLike, p: PointLike) -> Writer
def fragment(self, f: Fragment, *, prefix: str = "") -> Writer
# f.start 必须等于当前点;否则 ValueError(不自动连线)
def node(self, tag: str, **meta: float) -> Writer # 给“当前点”命名
def corner(self, tag: str, **meta: float) -> Writer # 当前点 = 角点槽(允许不连续)
def cusp(self) -> Writer # 当前点允许切线不连续,但不是槽
def end(self, tag: str, **meta: float) -> Writer # 下一段 = 端点槽(笔画切口)
def close(self, role: ContourRole = "ink") -> Writer # 闭合当前轮廓,可继续 start 下一个
def build(self) -> Drawing语义:
- 连续性在书写时检查,不在事后修。
close()和build()时检查每个 on-curve 节点:若相邻两段至少一段是曲线,则必须 G1;被corner()/cusp()标记的节点除外。失败抛ContinuityError,信息里带节点 tag 或坐标、实测角度。 - 两段都是直线的节点默认允许转角,不需要声明。
role="ink"的轮廓在build()时规范为逆时针,"counter"规范为顺时针(与geo.signed_area的符号约定一致——正为逆时针,方向反转时节点 index 同步重映射)。- 同一
Drawing内 tag 必须唯一;重复抛ValueError。 Writer不知道任何字母、风格或System。
ContourRole = Literal["ink", "counter"]
@dataclass(frozen=True, slots=True)
class Slot:
kind: Literal["corner", "end"]
tag: str
contour: int
node: int # corner:角点 index;end:切口段起点 index
meta: Mapping[str, float]
fill: str | None = None # 字形内显式指定的填充名,覆盖项目配置
@dataclass(frozen=True, slots=True)
class Drawing:
contours: tuple[Contour, ...]
roles: tuple[ContourRole, ...]
nodes: tuple[tuple[Node, ...], ...] # 每个轮廓的命名点
slots: tuple[Slot, ...]
def point(self, tag: str) -> Point
def find(self, pattern: str) -> list[Node] # fnmatch 风格匹配点分 tag
def outline(self) -> Outline # 交给 compile / optics
def signature(self) -> tuple # 兼容签名:每轮廓 (role, 段类型序列, tag 序列)
def __add__(self, other: Drawing) -> Drawing # 叠加(overlap 构造);tag 冲突报错
def translated(...); def mirrored_x(...) # 同 Fragment两个母版的 signature() 相等,就说明它们结构可插值(signature() 只做结构比较;插值检查由 axes.interpolatable 负责)。
解剖构件与诊断共用同一套节点 / 槽位标签语法(由 typekernel.anatomy.tags 解析;
诊断 tags.grammar 以 info 报告不合规标签):
tag := part ( "." qualifier )*
part := stem | arch | bowl | join | bar | diag | spine | hook | tail | ear
| spur | cross | dot | drop | aperture | serif | mark
qualifier:= side | position | face | endpoint | index
side := l | r | t | b
position := foot | top | crown | apex | vertex | crotch | entry | exit | stress
face := o | i # 外侧 / 内侧
endpoint:= a | b # 开口端点(仅 aperture)
index := [0-9]+
例:stem.l.foot.o、arch.crown.o、join.crotch.r、aperture.0.a。
诊断约定:stem.*.foot.o 与 stem.*.foot.i 之间是竖干宽度;join.crotch.* 是削薄点;
aperture.N.a 与 aperture.N.b 之间是开口宽度。新增 part 名必须先写入本节并更新解析器。
@dataclass(frozen=True, slots=True)
class CornerSite:
tag: str
point: Point
in_dir: Point # 进入角点的单位方向
out_dir: Point # 离开角点的单位方向
in_len: float # 进入边可裁剪的最大长度
out_len: float
role: ContourRole
meta: Mapping[str, float]
@dataclass(frozen=True, slots=True)
class CornerFill:
trim_in: float # 从角点沿进入边回退的距离
trim_out: float # 沿离开边前进的距离
path: Fragment | None = None # 替换路径;None + 零 trim = 几何 no-op(≡ plain / 未匹配)
class CornerFiller(Protocol):
def __call__(self, site: CornerSite, system: System) -> CornerFill: ...
@dataclass(frozen=True, slots=True)
class EndSite:
tag: str
a: Point; b: Point # 切口段两端(按轮廓方向)
a_dir: Point # 到达 a 的笔画边方向
b_dir: Point # 离开 b 的笔画边方向
role: ContourRole
meta: Mapping[str, float]
@dataclass(frozen=True, slots=True)
class EndFill:
trim_a: float; trim_b: float
path: Fragment | None = None # None + 零 trim = 几何 no-op(≡ 未匹配;退化参数同)
class EndFiller(Protocol):
def __call__(self, site: EndSite, system: System) -> EndFill: ...
def apply_slots(
d: Drawing,
system: System,
corners: Mapping[str, CornerFiller], # 键是 tag 模式,按插入顺序首个匹配生效
ends: Mapping[str, EndFiller],
named: Mapping[str, CornerFiller | EndFiller] | None = None, # 供 Slot.fill 显式引用
) -> Drawing规则:
- 未匹配任何模式的槽保持原样(等价于
plain)。显式plain()与退化参数(如rounded(radius=0)、ball(radius=0)、flare(amount=0, depth=0)、angled(deg=0)、flat(inset=0))必须走同一 no-op:trim=0且path=None,不插入零长段、不消费角点/端点槽。 - 填充后的轮廓仍要通过 6.2 的连续性检查;插件产生不连续视为插件 bug。零长段不得靠 post-fill「跳过」蒙混——故意保留的切线不连续只走角点槽豁免;line–line 接合不要求 G1。
- 填充是确定性的:同一
System结构下,不同母版填充出的段类型序列必须相同(插件契约测试覆盖)。 trim_in > in_len或路径首尾不落在裁剪点上 →SlotFillError。
字母无关。项目提供骨架,插件只负责笔模型展开为 Drawing:
def broad_nib(thickness: float, *, contrast: float = 0.55, angle_deg: float = 30.0) -> Pen
def pointed_pen(thickness: float, *, contrast: float = 1.0, angle_deg: float = 0.0) -> Pen
def expand_skeleton(centerline, pen, *, join="round", cap="round", miter_limit=4.0, chord_budget=None) -> Drawing
# closed + join="round" → ValueError (VF signature flip); use bevel/miter
# chord_budget=None validates closed sampling; "auto"|float densifies (centerline-stable)
def expand_skeleton_cubic(centerline, pen, *, join="round", cap="round", ...) -> tuple[Drawing, CenterlineExpandReport]规则:
- 复用
geo.stroke.Pen/Centerline/CubicCenterline/expand/expand_cubic;不 import starter,不出现字母名。 - 展开结果去掉近零长线段,并把剩余
line提升为共线 handle 的cubic(闭骨架禁止join="round"(工具直接报错);开骨架 round join 的 line/cubic 翻转仍经统一成 cubic 后保持Drawing.signature()跨字重稳定);单轮廓一律ink(必要时翻成 CCW);环:最大 |area| →ink,其余 →counter。 - 无角点/端点槽的 skeleton 字形:
Project可不配置corners/ends,从而跳过apply_slots(笔迹接合不是 Writer G1 链)。 blend_corners/ heal / seal 不是本插件路径。
全部只接收几何量,不接收 System,不出现字母名:
def tensioned_arc(p0, t0, p1, t1, *, tension: float = 0.55) -> Fragment
# 从 p0 沿 t0 出发、沿 t1 到达 p1 的单段三次;tension 为手柄长 / 弦长比例
def superellipse_quarter(center, rx, ry, quadrant: int, *, exponent: float) -> Fragment
# 复用 geo.curves.superellipse_kappa
def spring(stem_edge_x: float, y_from: float, y_to: float, toward: Point,
*, depth: float, tension: float) -> Fragment
# 直线笔画边进入曲线的过渡段(肩 / 碗接干处),保证 G1
def diagonal_band(p_top: Point, p_bottom: Point, *, weight: float, compensate: bool = True) -> tuple[Fragment, Fragment]
# 斜笔两条边,weight 为期望的视觉粗细,compensate 时按角度修正垂直宽
def hook(
stem_edge_x: float,
y_join: float,
tip: Point,
*,
depth: float,
tension: float = 0.55,
tip_tangent: Point | None = None,
) -> Fragment
# 竖直干边 (stem_edge_x, y_join) 出发的钩形终端:G1 离开干(竖直切线朝 tip.y),
# 经 depth 控制的腹点到 tip;默认 tip 切线水平回指干(回卷)。两段 tensioned_arc,腹点 G1。@dataclass(frozen=True, slots=True)
class Zones: base: float = 0; x: float; cap: float; asc: float; desc: float
@dataclass(frozen=True, slots=True)
class Overshoot: round: float; pointed: float
@dataclass(frozen=True, slots=True)
class System:
upm: float
zones: Zones
overshoot: Overshoot
stem: Mapping[str, float] # 自由键,如 "lc" / "lc_h" / "uc"
tokens: Mapping[str, float] = {} # 项目自定义(含 space_stem_mul 等)
slant: float = 0.0 # Δx/Δy 斜体倾角比;0=正体;供 audit/spacing/kern
def tuned(self, **kw) -> System
def slant_dx(self, y, *, y0=0.0) -> float
def map_xy(self, x, y, *, y0=0.0) -> tuple[float, float]
def stem_direction_deg(self) -> float # 斜体“竖笔”走向(正体=90°)不得有 aperture / roundness 这类“一个数影响多处几何”的旋钮。
slant 是几何事实(整副字的倾角),不是多处扇出的风格旋钮;斜体项目应设 System.slant,不要只把倾角藏进 tokens["slant"]。
Typed intent(Intent / Brief,typekernel.system.intent)把散文 brief 接到数值表:封闭词表映射到 slant / stem / pen_* / 宽比 token;tk audit 在越界时告警。不是美学打分,也不生成字形。
@dataclass(frozen=True, slots=True)
class Master:
name: str
system: System
def glyph(name: str, *, group: str = "") -> Callable[[GlyphFn], GlyphFn]:
"""Register a glyph builder. ``fn(system) -> Drawing``.
Attach master overrides with ``@fn.master("Black")`` on a second function
of the same shape. Glyph ``name`` is a production name (may be a letter in
*projects*); kernel tests must still use synthetic names(见第 7 节第 5 条)。
"""
@dataclass(frozen=True, slots=True)
class GlyphSpec:
name: str
group: str
build: Callable[[System], Drawing]
masters: Mapping[str, Callable[[System], Drawing]] # override builders
class Project:
def __init__(
self,
system: System,
glyphs: str | Path,
*,
masters: Sequence[Master] = (),
corners: Mapping[str, CornerFiller] | None = None,
ends: Mapping[str, EndFiller] | None = None,
default_advance: float | None = None,
) -> None: ...
def discover(self) -> None:
"""Import every ``*.py`` under ``glyphs`` and collect ``@glyph`` specs."""
def specs(self) -> Mapping[str, GlyphSpec]: ...
def evaluate(self, name: str, *, master: str | None = None) -> Drawing:
"""Run the glyph (or its master override) with the resolved System.
Applies ``corners`` / ``ends`` via ``apply_slots`` when configured.
"""
def to_glyph(self, name: str, *, master: str | None = None, advance: float | None = None) -> "font.Glyph":
"""``evaluate`` then wrap contours as ``font.Glyph`` (advance default:
bbox width, or ``default_advance``, or ``tokens['advance']``)."""规则:
@glyph注册发生在模块 import 时;discover()负责把glyphs/加进导入路径并加载。- 默认母版用
Project.system;masters里同名覆盖System,字形侧@build.master(name)覆盖画法。 Project/glyph/Master/GlyphSpec是内核公开符号。
tests/architecture/test_boundaries.py,对内核包生效:
- 禁止 import
typekernel.parts(对全仓生效)。 - 函数名不得匹配
^(build_[A-Za-z]|[A-Za-z]_|[a-z]+_from_[A-Za-z]|\w+_yoke)$。 - 源码不得出现
style.serif、italic_slant_deg、vf_compat。 plugins/<x>/不得 importplugins/<y>/;任何新包不得 import 其他模块的_私有名。- 内核测试(
tests/draw、tests/system、tests/project)里不得出现字母字形名字符串(用一个小正则:单字母 glyph 名或uniXXXX);需要形状时用合成夹具。 - 构件不组字:
anatomy公开函数只返回Drawing或构件 dataclass;函数名不得含字形名,也不得以glyph_/build_开头。 - 字形名字面量:
geo/draw/system/project/font/compile/cli/plugins/perceive/diagnose/review(及目标架构中的后续包)的字符串字面量不得是单字母或 AGL 字形名(f-string 片段与 TypeVar 名除外)。光学 stem 种类由Project.audit.stems声明,内核不再按字母名猜测。 - S2 例外 —
typekernel.knowledge:该包是纯数据先验(Unicode→拓扑期望 / confuse pairs),允许出现字母 / AGL 名作为数据字段。禁止把 knowledge 当作构造入口;构造器与其它内核包仍适用第 7 条。
用 ast 扫描实现,不额外引入依赖。
全部使用合成形状。
| 测试 | 断言 |
|---|---|
| Fragment 首尾不接 | ValueError |
| Fragment 内曲线接合不 G1 且无 cusp | ContinuityError |
reversed / mirrored_x |
段几何正确、节点 index 正确重映射、tag 保留 |
| Writer 画矩形 | 4 直线、无需声明角点、逆时针 |
| Writer 画“直线进曲线”且不 G1 | ContinuityError,信息含坐标 |
同上但标 corner() |
通过,生成一个 corner 槽 |
fragment() 起点不等于当前点 |
ValueError |
role="counter" |
顺时针;节点 index 随反转重映射 |
| tag 重复 | ValueError |
signature() |
参数不同(宽度 / stem)但同一书写程序 → 签名相等;多一段 → 不等 |
Drawing + Drawing |
轮廓叠加、tag 冲突报错 |
apply_slots 用测试内的假填充 |
角点被替换、仍连续、未匹配槽不变、trim 越界报错 |
tensioned_arc / superellipse_quarter / spring |
端点、端切线正确;与相邻直线 G1 |
hook |
干接合 G1;终点 = tip;腹点在干同侧且 |
diagonal_band(compensate=True) |
垂直于笔画方向的实测宽度 = weight(容差 0.5 单位) |
| 单轮廓合成样例 | 一个“直干 + 拱 + 直干”的合成单轮廓(测试里叫 arch_body,不叫 n),≤ 40 行,全部节点 G1 或已声明角点 |
每条 PR 都对照这几条;任何一条被违反,说明在往回走:
- 内核与插件里不出现字母名(S2 knowledge 例外除外)。 没有
build_H/*_from_n/n_yoke这类函数,也没有对某个风格值的if分支。字母几何只存在于项目(starter)里;字母 名 作为数据只允许出现在typekernel.knowledge。 - 不允许事后修缝。 连接难看就改画这个连接的片段或元素,不加 seal / soften / relieve / blend 这类补丁。
- 一个字形的几何不由另一个字形的函数隐式推出。 复用靠显式 import 元素,不靠
x_from_y(...)里藏几百行。 - 新加的参数要对任何合规项目都有意义,不能只对库自带的那副字有意义。
- 内核测试不出现具体字母,也不锁定某副字的轮廓哈希或段数。 需要形状时用合成夹具。
- agent 要改一个细节,改的是自己项目里的文件,不需要改库。
- 不做 GUI 编辑器;agent 的“编辑器”就是字形文件 + 样张。
- 不在内核里放“能自动生成全套字”的工厂(包括“方便起见”的
build_all)。 - 不用数值门禁冒充美学判断;美学由视觉审阅决定。
- 不做
style_consistency/originality一类美学 AI 分;Intent只做 brief→System 的显式映射与越界告警。 - 不为兼容旧 API 长期维持双轨:旧路径直接删除,git 历史即存档。
已实现(细节以源码与 tk --help 为准;原目标架构中的感知与诊断内容已并入本节,不再重复维护):
| 能力 | 入口 |
|---|---|
| live 渲染 / 编辑视图 | typekernel.perceive · tk see |
| 测量报告 | tk explain --json |
| 诊断 v2(拓扑 / 区线 / 间距 / 灰度 / 插值) | tk audit --json/--masters/--strict |
| 评审与任务板 | tk review · tk status · tk critique |
| shipped 样张 | tk proof(perceive.specimen;qa.proof 为兼容 shim) |
新增 starter 与风格插件的限制已解除。
| 能力 | 入口 |
|---|---|
| 标签语法 | typekernel.anatomy.tags · tags.grammar info |
| 解剖构件(直线) | stem crossbar dot drop |
| 解剖构件(曲线) | arch bowl bowl_join open_bowl/BarSpec spine |
| 解剖构件(斜笔) | diagonal_pair crossing |
| 解剖构件(钩尾) | hook tail ear spur |
| latin-sans 控制字 anatomy 化 | 程序侧通过;人工批准待定(approvals/latin-sans-control.json) |
| latin-sans 字组 v2 | 程序侧通过(lc/uc/figs/punct 全组 + reviews);全字符集样张人工批准待定 |
| 三 starter 标签语法 | 已合法(tags.grammar 0;latin-sans / humanist-italic / chancery) |
| 插件语义 / slab litmus | 已落地(slab/bracketed/rounded) |
| designed masters | 已落地(Light/Bold 宽/开口/张力);阶梯人工批准待定(approvals/latin-sans-ladder.json) |
| 能力 | 入口 |
|---|---|
marks/ 发现 + mark advance 0 |
ProjectSession(trait mark);槽位填充跳过 mark |
| 锚点检查 | diagnose.checks.anchors.AnchorsCheck |
| latin-sans 组合附加符 | starters/latin_sans/marks/(12 comb + .case);基字 zone_anchors |
compose / composites |
已落地(project.compose;latin-sans ~160 composites) |
charset + tk status 区块覆盖率 |
已落地(latin-1 + ext-a 319/319) |
| 特性后缀生成 + fea 汇编 | 已落地(generate_features / assemble_features / features.syntax) |
| latin-sans ss01 / tnum / onum / case | 已落地(a.ss01、*.tf/*.osf、*.case punct) |
| kern / VF / FontBakery | 已落地(tk kern suggest、wght 100–900 可变字体、tk check --release) |