** This file was created with an agent. **
anthropic/claude-fable-5.1 (goose)2026-09-19-initial-analysis.md,
authored by moonshotai/kimi-k3 (referred to below as Kimi).The operator asked for an independent repeat of the research behind the
initial analysis: same three questions (purpose / why annoying / how to
refactor), same working tree (dbe55886 plus the uncommitted
horizontalLayout work in type-spec/models.mjs, properties-generators.mjs,
defaults.mjs, registered-properties.mjs), and a comparison against
Kimi’s conclusions.
Method: single-agent, read-only survey — no subagents. I read the core
definitions (registered-properties-definitions.mjs,
registered-properties.mjs), every get*PPSMap factory, all getDefaults
implementations (defaults.mjs, type-tools-grid.mjs, videoproof,
motion-stage, node-specs, style-patches, ui-contextual-template),
the resolver table in type-driven-ui-basics.mjs, the scope machinery
(scope-resolution.mjs, type-specnion.mjs, live-properties.typeroof.jsx,
meta.typeroof.jsx), the generators (properties-generators.mjs), and
git log for the files/commits Kimi cited. Every file:line reference in the
initial document was re-checked; the results are in section 1.
Structure of this document:
Where I quote or paraphrase the initial analysis, it is attributed as [Kimi]. Everything not so marked is mine.
| [Kimi] claim | Status | Note |
|---|---|---|
Three namespaces (model field / registry [prefix, registryKey] / runtime fullKey) joined by ProcessedPropertiesSystemRecord |
✔ | Accurate reading of registered-properties-definitions.mjs:102-138. |
ProcessedPropertiesSystemMap is the iteration source for _defineGenericWidgets |
✔ | type-driven-ui-basics.mjs:341-370. Note it also accepts a bare string and calls fromPrefix itself (line 346-348). |
Registry entries carry default/inherit/label/min/max/step/unit; no validation |
✔ | |
Effective map is CascadingMap([local, inherited, defaults]) |
✔ | scope-resolution.mjs:546-550. |
Root defaults layer is built by running the same generators over a synthetic primal draft |
✔ | defaults.mjs:166-268. |
inheritancePolicyGen tombstones inherit: false properties |
✔ | properties-generators.mjs:731-735. |
Self-documented “double use” of prefix, fullKey FIXME, createSimpleRecord “hopefully temporary”, _PPS_ENTRY_MARKER “until fully integrated” |
✔ | def.mjs:114, 173-187. _PPS_ENTRY_MARKER is written but never read anywhere in lib/ — it is dead, not merely temporary. |
| ~8 near-identical factory loops | ✔ | Actual count: pps-maps.mjs ×2, fundamentals.mjs ×2, type-tools-grid.mjs ×2, models.mjs ×1, ui-horizontal-layout.typeroof.jsx ×2 (both one-liners around fromPrefix) = 9. |
Two dispatchers getPPSMapForModel(parentPPSRecord, FieldType) (grid) vs _getPPSMapForModel(FieldType, ppsRecord) (fundamentals, ui-horizontal-layout) with swapped argument order |
✔ | Real footgun; confirmed. The ppsMap resolver in type-driven-ui-basics.mjs:220-221 calls the injectable variant with grid order. |
_excludesTypeSpecPPSMap, _skipPrefix, _skipFullKey as hidden policy |
✔ | Also _excludesNodeSpecPPSMap, _excludesTypeToolsGridPPSMap (“I did not sanity check this, it’s just copy pasta”). |
Dead parentPPSRecord stub in getTypeSpecPPSMap propagated by copy |
✔ | Present in pps-maps.mjs ×2 and type-tools-grid.mjs:getGridPropertiesPPSMap. |
Raw string prefixes 'axesLocations/', 'intentStyleLinks/', 'markStyleLinks/' with “we should use a symbol here!” |
✔ | Interesting detail: INTENT_STYLE_LINKS/MARK_STYLE_LINKS constants do exist (def.mjs:38-39) but pps-maps.mjs:53-56 still uses literals. |
Growth path type-spec-ramp → grid (28c47aa2) → …; 25a8fe79 consolidation |
✔ | Commit messages verified. |
type-specnion.mjs is now only re-export aliases of
scope-resolution.mjs”. Not quite: it also owns the
DEMARCATION_PROPERTY / DEMARCATION_INHERITANCE / TOMBSTONE symbols
(type-specnion.mjs:7-11), which scope-resolution.mjs imports back. The
dependency is circular-ish by design (symbols module ↔ machinery module), not
a leftover shim.fullKey … registryFullKey (${prefix}${registryKey})”.
Correct, but the more important observation is which one each consumer
uses — see ⚠ 2.3: defaults.mjs looks the registry up by fullKey,
type-tools-grid.mjs by registryFullKey. Only the latter is correct for
re-rooted records.LANGUAGE among the registry prefixes in passing; it is in
fact not in SYMBOLIC_TO_PREFIXES (def.mjs:40), so
getFromRegistry refuses it even though REGISTERED_PROPERTIES[LANGUAGE]
exists (registered-properties.mjs:133). The [LANGUAGE] registry block is
therefore unreachable through the public API. Small, but it shows the
registry’s “which prefixes count” gate is a second, separate policy list.These are the points where I come to a different conclusion than the initial analysis. They are called out prominently because they change what a refactoring should target.
[Kimi] wrote: “every widget gets a pre-bound getDefault … that
pull-reads the parent’s effective propertiesMap by fullKey … So inheritance
exists twice by design: push (the scope cascade computes effective values) and
pull (widgets query the parent’s effective map for display).”
I disagree. typeSpecGetDefaults reads getLiveProperties(), and in
type-spec-properties.typeroof.jsx:248-258 that is
this.getEntry(\typeSpecProperties@${typeSpecPath}`)— theTypeSpecLiveProperties registered **for the typeSpec being edited itself** (meta.typeroof.jsx:263, key widgetBus.rootPath), not for its parent. Its typeSpecnion.getProperties()is the own scope'sCascadingMap([local,
inherited, defaults]). When the model field is empty, the generator does not yield a local value, so the read falls through to inherited/defaults`
— which is exactly the push result.
So the widget path is a plain read of the already-computed cascade. There
is one inheritance mechanism, and getDefaults is a view on it. The
FIXME: … Better then hard-coding the parent path in here comment in
defaults.mjs:84-86 (and its copy in type-tools-grid.mjs) describes a
state of affairs that no longer exists; the “root gives KEY ERROR” case is
handled by typeSpecPropertiesKey === "typeSpecProperties@" → null.
Consequence for refactoring: Kimi’s strategic item “Unify push/pull
defaults” (section 4 F) has no target. What can be unified is the
per-prefix branching inside the getDefaults copies (Kimi’s B), which is a
different, smaller problem.
fromPrefix confusion is a symptom; the cause is an unstated generator ↔ ppsMap contract[Kimi] identified the concrete symptom correctly: “prefix = GENERIC
while fullKey = ${parentPPSRecord.propertyRoot}${fieldName} — two different
bases for two roles … fromPrefix cannot express that combination, which is
exactly the confusion that triggered this research.”
I read the cause differently. For ui-horizontal-layout.typeroof.jsx,
fromPrefix(GENERIC, …) is correct, not a limitation:
manualHorizontalLayoutRunion (properties-generators.mjs:488-560) yields
flat keys — generic/columnCount, generic/lineLength,
generic/columnGutter, generic/inlineMargins/start — not
generic/horizontalLayout/columnCount. So the fullKey a widget must read
is ${GENERIC}${fieldName}, and that is what fromPrefix produces.
The leading case is different because leadingGen
(properties-generators.mjs:250-291) yields nested keys:
leading/leading/algorithm, leading/leading/line-height-em,
leading/leading/minLeading, …. Hence getManualLeadingPPSMap must
re-root fullKey under parentPPSRecord.propertyRoot and additionally remap
leading → line-height-em.
In other words: a ppsMap has to mirror the key layout that the matching
generator yields, and nothing in the code states, enforces, or even
co-locates that pairing. getManualLeadingPPSMap lives in
type-spec/fundamentals.mjs; leadingGen lives in
layouts/type-stage/properties-generators.mjs; the registry entry
[GENERIC].leading / [LEADING].leading lives in a third file. Copying
the leading factory as a template for horizontal layout therefore looks
plausible and is wrong — precisely what happened. The fromPrefix API is
not the problem; the missing contract is.
This also explains why defaults.mjs:getTypeSpecDefaultsMap needs
special-case branches per fullKey (blockMargins, inlineMargins,
columnGutter, lineLength, horizontalLayout): each is a struct whose
generator flattens it in its own way, and the defaults builder has to
un-flatten by hand.
fullKey is broken for re-rooted records (bug, not just smell)Not covered by [Kimi].
getFromRegistry splits the identifier at the first /
(def.mjs:59-72): generic/horizontalLayout/columnCount →
['generic/', 'horizontalLayout/columnCount'] → registry miss →
falls back to modelDefaultValue or throws KEY ERROR.
defaults.mjs:getFallback(fullKey, …) and typeSpecGetDefaults pass
fullKey. For every record whose fullKey is re-rooted under a parent
(getManualLeadingPPSMap, getAutoLinearLeadingPPSMap,
getLineWidthLeadingPPSMap, anything built from parentPPSRecord), the
registry default / label / unit is unreachable through this path.
It works today only because those UIs get label/unit via the resolver
table, which calls getRegisteredPropertySetup(ppsRecord) → uses
registryCredentials (correct), and because the live map usually has the
value.type-tools-grid.mjs:typeToolsGridGetDefaults already does it right:
_getFallback(registryFullKey, …) (lines 1096, 1142). The two “copies”
Kimi called near-verbatim differ in exactly this semantic detail.isInheritingPropertyFn(property) (registered-properties.mjs:442-447)
has the same flaw: for any nested key the lookup misses and the fallback
{inherit: true} applies. Every nested key silently inherits,
regardless of registry intent. The special case for COLOR (split('/', 2)) is a one-off patch for the same class of problem.This is a prerequisite fix for any refactoring that wants to lean on the registry as the single policy location (Kimi’s E).
prefix carries three semantics, not two[Kimi] described the “double use” of prefix (registry group +
path base). There is a third: behavioral flag. SPECIFIC means “no
registry default, caller must supply one”; 'axesLocations/',
OPENTYPE_FEATURES, LANGUAGE mean “value is font-dependent, key is
${prefix}${fieldName} not fullKey”; DIMENSION in grid is “log this
case”. These branches are what the getDefaults copies and _skipPrefix
actually switch on. Splitting the record into registryRef + propertyKey
(Kimi’s C) does not address this axis; it needs its own explicit field or
strategy object.
registryKey is almost unused. The remap facility that motivates the
record (“different values/instances can use the same setup”) is exercised
in exactly one place: type-tools-grid.mjs:getPPSMapForModel
(gridAxisType, gridAxisSelection, start/end). Everywhere else
registryKey === null → modelFieldName. The abstraction is paid for
everywhere and used once.modelFieldName on the record is redundant with the ppsMap key it is
stored under; def.mjs:189-191 already says “this may not be required at
all”. Consumers pass fieldName separately to getDefaults anyway.fieldName is a mis-named parameter in getDefaults. In the
axesLocations//OPENTYPE_FEATURES/LANGUAGE branch it is an axis tag
or feature tag ('YTFI', 'kern'), not a model field; elsewhere callers
pass null (“fieldName not required here”). It is a per-prefix sub-key.createSimpleRecord(prefix, \@${newValue}`)(type-bound default via[LEADING][‘@AutoLinearLeading’]), createSimpleRecord(SPECIFIC, ‘font’), createSimpleRecord(GENERIC,
‘fontSize’), createSimpleRecord(root, ‘irrelevant’)— the record is being used as a *lookup handle* forgetDefaults, which then only reads .prefixand.fullKey. A two-string tuple would do; the class adds ceremony without adding checks (the constructor only validates that prefixis notundefined`).ui-contextual-template opts out entirely. Its injected getDefaults
ignores the property stream and reads the model directly via
Path.fromString(ppsRecord.fullKey).toRelative(propertyRoot). The comment
(“Maybe a good learning for the next system: include a full path to the
source model?”) is a design signal: for pure-UI containers the PPS is
being used only as a path encoder.ppsMap resolver hard-codes a model. type-driven-ui-basics.mjs: 217-218 special-cases LineWidthLeadingModel before consulting
injectable.getPPSMapForModel. The 20-line comment above it is the most
honest description of the open design question in the codebase.horizontalLayoutRunion and the
horizontalLayout branch in getTypeSpecDefaultsMap contain
console.log calls; [GENERIC].horizontalLayout.default is a nested
struct default, i.e. another case for the un-flattening branch list. Not a
PPS problem, noted for cleanup.Kimi’s items A–F are kept as reference points; my assessment follows each.
PATH_SPEC_AUTO_LINEAR_LEADING in synthetic-values.mjs,
which already is that spec for leading). A table that lives in a UI module
reproduces the current problem with less code.getDefaults. Agree. Precondition:
settle ⚠ 2.3 first (registryFullKey vs fullKey), otherwise the unified
function inherits the wrong behavior from defaults.mjs. The
node-specs.typeroof.jsx:specGetDefaults (no live properties) is the
minimal core; the others add the live read plus per-prefix handlers.registryRef from propertyKey. Agree, and add the
third axis from ⚠ 2.4 (read strategy). A record then has: where to find UI
metadata, where to read/yield the value, how to read it. Rename
fullKey → propertyKey as the FIXME asks. Drop modelFieldName from the
record (section 3).getStyleLinks
helper and pathSpec*Gen functions show this is already half-happening.
Use the existing INTENT_STYLE_LINKS/MARK_STYLE_LINKS constants in
pps-maps.mjs; add AXES_LOCATIONS.SYMBOLIC_TO_PREFIXES gate (1.2), which is itself a policy list that would
need to move.New, mine:
LeadingAlgorithmModel, HorizontalLayoutAlgorithmModel,
ManualBlockMarginsModel, InlineLengthValueModel, …) gets one
co-located definition: key layout (flat vs re-rooted; renames such as
leading → line-height-em), from which both the generator’s yield
keys and the ppsMap fullKeys are derived. getTypeSpecDefaultsMap’s
branch list then becomes a lookup over the same definitions. This is the
only direction that prevents the copy-the-wrong-template failure mode.getFromRegistry/isInheritingPropertyFn key resolution.
Either look up by registryCredentials everywhere (records are available
at all getDefaults call sites) or make _getPrefixAndName aware of
nested keys. Small, independent, and it corrects a silent inheritance bug._PPS_ENTRY_MARKER, the parentPPSRecord
stubs in pps-maps.mjs/type-tools-grid.mjs, the 'irrelevant' record in
ui-char-groups.mjs, and the console.logs in the uncommitted
horizontalLayout code. None of this needs design work.Suggested order: H, I (independent, small) → G for one field type as a pilot (leading, since the pathSpec already exists) → B → A/C/D together → E.
Single-agent survey, all file:line references taken directly from the
working tree on 2026-09-19 (HEAD dbe55886 + uncommitted changes). The
initial analysis was re-read after my own survey, not before, to reduce
anchoring; agreement in section 1 is therefore independent confirmation
rather than acceptance.