fix(docs): validate the singular framework field and collapse five framework maps into one - #430
Open
eugenia-scandit wants to merge 8 commits into
Open
fix(docs): validate the singular framework field and collapse five framework maps into one#430eugenia-scandit wants to merge 8 commits into
eugenia-scandit wants to merge 8 commits into
Conversation
…values docs-schema.yml defined the plural `frameworks` array with an enum but never defined the singular `framework` field at all. With additionalProperties: true it got no validation, and its values drifted. Canonical set: the enum is now the single source of truth for framework identifiers, and singular and plural share it - a page states one platform or several, never two different vocabularies. Normalized 117 pages: - netIos -> net-ios (31), netAndroid -> net-android (31), react -> react-native (30). Pure spelling drift; the canonical spellings already existed on other pages. - express (13) and bolt (12) were never frameworks. Those pages live under docs/hosted/, so they are `framework: hosted` with the value they were carrying moved to `product: express` / `product: id-bolt`, both of which already exist in the product enum. Two things the ticket's count could not have seen: - `kmp` is a sixth non-canonical value, on 37 pages - more than any entry in the ticket's table. It is not drift: those pages are the published /sdks/kmp/ tree, kmp is first-class in FRAMEWORK_MAPPING, QUERY_FRAMEWORK_TO_PATH and SkillsCallout, and being unreleased is already tracked by UNRELEASED_FRAMEWORK_SLUGS. The enum was stale, so kmp is added to it rather than the pages being rewritten. - FW_TO_PJ does not exist on main - PR #417 is unmerged. The real duplication is five maps, not three. Why nobody ever saw an error: docs-gate is a ratchet over changed files, so 37 pages violating the *plural* enum went unreported for months. Step 2 of the ticket alone would not have caught the other 117 either. scripts/verify-frameworks.cjs (yarn verify:frameworks, wired into CI) therefore checks the whole corpus, and the code maps as well: CONTENT no page may declare a framework outside the enum DRIFT no code map may key off a framework the enum does not define COVERAGE enum slugs missing from a map are reported as known gaps It found one live bug on its first run: FRAMEWORK_MAPPING had no `linux` entry, so parseSdksRoute() resolved no framework at all for every /sdks/linux/ page. Fixed here. Ticket acceptance test: a page set to `framework: netIos` fails the gate and passes after normalization - verified both ways. Step 4 (consolidating the five maps onto one) is deliberately NOT in this commit. Those maps have different membership, so merging them changes runtime behaviour - the linux gap above is exactly that kind of change - and it deserves its own reviewable diff. The DRIFT check above is what stops them diverging further in the meantime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 4 of the ticket. The framework set was written out five times -
FRAMEWORK_MAPPING, QUERY_FRAMEWORK_TO_PATH and HOMEPAGE_FRAMEWORK_ALIASES
in components/utils/frameworks.ts, plus FRAMEWORK_URL_PATH and
FRAMEWORK_SLUG in SkillsCallout - each keyed slightly differently and
nothing tying them together. That is why `linux` could be missing from one
of them, and only that one, for the entire life of the page.
src/constants/frameworks.ts is now the single registry. Each entry carries
slug, display name, route segment, whether an Agent Skills page exists,
whether it is unreleased, and any alternate spellings. All five maps are
derived from it, as is UNRELEASED_FRAMEWORK_SLUGS.
`slug` is the same string as the docs-schema.yml enum value, so the
frontmatter vocabulary and the code vocabulary are now one vocabulary.
yarn verify:frameworks compares the registry against the enum in both
directions and fails on any difference - a slug in the registry the schema
does not allow, or a slug the schema allows that no component can resolve.
The risk in this refactor is not a crash, it is a map silently gaining or
losing an entry, so scripts/test-frameworks.cjs (yarn test:frameworks,
wired into CI) pins the expected membership of every derived map against
what the hand-written maps held before. It loads the real modules through
the TypeScript API rather than re-deriving anything, so it fails if the
registry changes shape. 9 assertions, all passing, including the .NET
two-segment route, the homepage aliases, and the Agent Skills exclusions.
One deliberate membership change: `hosted` now counts as a framework
without Agent Skills. It is not a homepage selector slug, so nothing
passes it to frameworkHasAgentSkills - inert today, and correct if it ever
is passed.
Retargeting the gate also surfaced the same parser trap twice: anchoring
on the first `{` or `[` after a name reads the TYPE annotation, not the
value, and yields zero entries while reporting success. Both readers now
anchor on the assignment and fail loudly on zero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ratchet checks a whole file as soon as a PR touches one line of it. That is right for prose someone is actually editing, and wrong for a mechanical metadata pass: normalizing `framework:` across 117 pages dragged in 233 pre-existing Vale findings the change neither caused nor altered (measured identical before and after: 233 either way). Nobody writes prose in frontmatter, so when a file's body is byte-identical to the ratchet base there is no prose to review. Those files now skip Vale and cspell. Schema and link checks still run on every changed file, and a file with any body edit is checked in full - verified by adding one sentence to one file and watching both new findings appear while the other 116 stayed skipped. Line endings are normalized before comparing: `git show` returns the repo blob with LF while the Windows working copy has CRLF, which made every file compare as changed and the skip silently never fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
…t it Two gaps left by the registry refactor, both the same shape as the bug it fixed: a framework name that resolves to nothing, with no error. 1. THE REGISTRY WAS RUNTIME-ONLY. `slug: string` meant verify:frameworks guarded the frontmatter and the schema, but nothing guarded code. A component writing `frameworks: ['Web']` for `['web']` compiled fine and then matched no framework - exactly how `netIos` and `react` got into the frontmatter. `FrameworkSlug` closes that: 4 typo cases that used to compile are now errors, with "Did you mean" suggestions. The union is spelled out rather than derived. Deriving it needs `as const`, which turns FRAMEWORKS into 13 exact tuple members and drops the optional `aliases` / `unreleased` keys from the entries that omit them - `f.aliases` and `f.unreleased` stop typechecking. Verified: that route fails `tsc` in two consumers. So the union is a second copy, guarded the way the schema enum already is, by a new three-way check (union / registry / enum, every pair both directions). 2. products.json AND features.json WERE OUTSIDE THE REGISTRY. Both state per-framework availability keyed by DISPLAY name, so the slug enum cannot see them - a second vocabulary, unverified. It had already drifted. features.json carried `.Net iOS` and `.Net Android` against the registry's `.NET iOS` / `.NET Android`, 16 keys each. Nothing matched them: FeatureList looks up `feature.frameworks[currentFramework]` with a display name from the registry, so those 32 rows were unreachable. 32 keys normalized; the new DATA check keeps the two files inside the vocabulary. The check is one-directional on purpose: a product need not support every framework, so a registry display missing from a data file is fine. Only a name the registry does not know is an error. UNRELEASED_FRAMEWORK_SLUGS is annotated `string[]` explicitly. It is a membership-test array queried with raw route segments; narrowing it to FrameworkSlug makes `.includes(someString)` an error at its call site. Both new checks verified in both directions: - pre-fix features.json -> exit 1, naming both bad keys; post-fix exit 0 - union with `linux` renamed -> exit 1, reporting the extra and the missing Gates green: verify:frameworks, test:frameworks (9), tsc --noEmit. Unblocks #427, which adds `frameworks?: string[]` to AgentInstall - a sixth copy of the vocabulary landing just as this PR removes five. It can now import FrameworkSlug instead. Not fixed here, needs its own change: FeatureList derives the framework from `/sdks/([^/]+)/`, which yields `net` for /sdks/net/ios/ and matches no entry, so the feature table is empty on both .NET frameworks regardless of this normalization. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FeatureList detected the framework with `/\/sdks\/([^\/]+)\//` - a single
path segment. For /sdks/net/ios/... that yields `net`, which is not a
framework, so `FRAMEWORK_MAPPING['net']` was undefined and every feature
was filtered out. The feature table has been empty on both .NET platforms
for the life of those pages, and normalizing the features.json keys in the
previous commit did not help: the lookup key never arrived.
Formalized in the registry instead of patched in place. `routeSegment`
already carries the two-segment shape (`net/ios`), so path resolution can
be derived from it rather than restated:
frameworkFromPath("/sdks/net/ios/add-sdk") -> net-ios / ".NET iOS"
Matching is longest-`routeSegment`-first so a one-segment framework can
never shadow a two-segment one, and it anchors on `sdks/` rather than the
start of the path, so `/next/` and `/7.6.14/` prefixes work. A future
multi-segment framework needs no change here - which is the difference
between this and the regex it replaces.
Measured end to end against features.json, counting features a .NET page
would render:
/sdks/net/ios/sparkscan/intro old: 0 new: 16
/sdks/net/android/matrixscan/intro old: 0 new: 16
/next/sdks/net/ios/add-sdk old: 0 new: 16
/sdks/ios/sparkscan/intro old: 16 new: 16 (unchanged)
4 new assertions: the two-segment routes, single-segment routes still
resolving, version-prefixed paths, and that `/sdks/net/` alone resolves to
nothing rather than to a framework. 13 passing.
parseSdksRoute is deliberately left alone. It hardcodes `(?:net\/)?` in
its regex, so it happens to be correct today; rewriting it onto
frameworkFromPath would change what it returns for paths with no product
segment, and there is no bug to justify that risk here.
Gates: verify:frameworks, test:frameworks (13), tsc --noEmit, full build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two functions answered "which framework is this /sdks/ path" with two
different regexes. FeatureList's captured a single segment and was wrong
for .NET; parseSdksRoute hardcoded `(?:net\/)?` and then undid it with
`.replace('/', '-')`, so it was right by coincidence of maintenance rather
than by construction. Either could drift again, and one already had.
frameworkFromRouteTail() in the registry is now the only place that maps a
route segment to a framework. Both callers use it:
frameworkFromPath() - unanchored, for a bare pathname (FeatureList)
parseSdksRoute() - anchored, plus the product segment
parseSdksRoute keeps every behaviour it had. Its regex now captures the
whole tail and the registry resolves the framework from it, but the anchor
stays (so /foo/sdks/ios/... is still not a route), the product segment
stays mandatory (/sdks/ios/ is still {}), URL_PRODUCT_MAPPING still
rewrites `matrixscan` and `label-capture`, and `lastSegment` is still
absent rather than undefined when there is no third segment.
Proved rather than asserted: the 15 inputs in PARSE_SDKS_ROUTE_BASELINE
were captured from the old implementation before touching it, and are now
compared with deepStrictEqual. A second test walks every routed framework
and checks both parsers return the same one for the same path - the
disagreement that caused the .NET bug cannot recur silently.
Also derives DocItem's SKILL_LESS_FRAMEWORK_PREFIXES from `agentSkills`
instead of listing ['/sdks/titanium/', '/sdks/linux/'] by hand. Same class
of copy, and the registry already knows. Pinned to the previous literal.
16 assertions passing (was 13). Gates: verify:frameworks, test:frameworks,
tsc --noEmit, full build.
Known and left alone: DocItem matches those prefixes with
pathname.startsWith(), so they miss version-prefixed paths like
/next/sdks/titanium/. Pre-existing, unchanged here, and fixing it changes
what the callout does on versioned pages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…inux The callout was suppressed by startsWith() over ['/sdks/titanium/', '/sdks/linux/']. A docs-version segment defeats that: /next/sdks/titanium/... and /7.6.14/sdks/linux/... do not start with those strings, so the Agent Skills banner appeared on the two frameworks that have no Agent Skills page at all - on every version except the one served at the site root. Same class as the .NET FeatureList bug: a path matched by hand instead of through the registry. Now resolved with frameworkFromPath(), so the `agentSkills` flag decides and every docs version behaves the same: /sdks/titanium/core-concepts hidden (unchanged) /next/sdks/titanium/core-concepts hidden (was SHOWN) /7.6.14/sdks/linux/overview hidden (was SHOWN) /sdks/ios/add-sdk shown (unchanged) /next/sdks/net/ios/add-sdk shown (unchanged) This is a deliberate behaviour change, confirmed as the intended behaviour: a framework with no Agent Skills should never advertise them. The derived SKILL_LESS_FRAMEWORK_PREFIXES list is gone rather than fixed - there is nothing left to keep in step. Test extended to assert both directions across versioned paths. Gates: verify:frameworks, test:frameworks (16), tsc --noEmit, full build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five copies were collapsed onto the registry. Three were left, and a scan
found them: FrameworksName, SearchBar's API_FRAMEWORK_LABELS, and
useFrameworkItems' switcher list. They are guarded here rather than moved,
because moving them changes behaviour and checking them cannot:
- FrameworksName carries umbrella members (`net`, `xamarin`) that label
grouping cards, not frameworks, and is keyed camelCase. It also indexes
9 components as FrameworksName[card.framework].
- SearchBar's keys are API-reference tokens (`dotnet.ios`) set by the
generator, not by us. Its regex tokens are hand-tuned for match order
and context, which mechanical derivation would lose.
- useFrameworkItems must keep Xamarin: versioned_docs/version-6.28.11 and
7.6.14 still ship /sdks/xamarin/{ios,android,forms}, and the switcher
shows them only on those versions. Dropping Xamarin would orphan real
pages. Icons cannot move either - the registry is import-free so
docusaurus.config.ts can read it under Node.
So the same treatment the schema enum and the FrameworkSlug union already
get: the copy stays, a gate compares it. Values must be registry display
names; useFrameworkItems' `slug` must be a registry routeSegment. Legacy
Xamarin routes and the umbrella displays are named exemptions, not silence.
Reading is scoped, not file-wide: API_FRAMEWORK_LABELS is located by name
and read by brace matching, because a line-level scan over SearchBar also
picks up `interaction: "mouse"` and the `fw: "net/ios"` query tokens as if
they were framework names. A renamed or reshaped list reports "unchecked"
rather than passing empty - verified by renaming the const.
Verified in both directions. Broke one value in each of the three files:
frameworksName.ts display name "iOSS" is not in the registry
SearchBar/index.js display name "Webb" is not in the registry
useFrameworkItems label "Linuxx" is not in the registry
useFrameworkItems route "linuks" is not in the registry
exit 1 with all four, exit 0 restored. No drift exists today.
Gates: verify:frameworks, test:frameworks (16), tsc --noEmit, full build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the ticket on the unvalidated
frameworkfield.The problem
docs-schema.ymldefined the pluralframeworksarray with an enum but did notdefine the singular
frameworkfield at all. The schema allows undeclaredfields, so it got no validation and its values drifted. Components that read it
silently found no match and fell back to their default — always iOS. No crash,
no error.
What this changes
docs-schema.ymlis the single source of truth; singular andplural share one vocabulary.
netIos/netAndroid/react),and 25
express/boltpages which were never frameworks — they live underdocs/hosted/, so they are nowframework: hostedwith the value they carriedmoved to
product:.(
src/constants/frameworks.ts), whichUNRELEASED_FRAMEWORK_SLUGSalsoderives from.
Three things the ticket could not have known
kmp— 37 pages, more than any row in the ticket's table. Not drift: it isthe published
/sdks/kmp/tree, it is first-class in the code, and"unreleased" is already tracked by a separate mechanism. The enum was stale, so
kmpwas added to it rather than the pages being rewritten.FW_TO_PJdoes not exist — PR feat: add ProductChooser ("Choose this if / not for" + framework matrix) #417 is unmerged. There were five copies,not three.
FRAMEWORK_MAPPINGhad nolinuxentry, soparseSdksRoute()resolved no framework at all for every/sdks/linux/page.Why nobody ever saw an error
docs-gateis a ratchet over changed files. Those 37 pages had been violatingthe plural enum for months and were never once reported. So
yarn verify:frameworks(in CI) runs over the whole corpus and over theregistry: a value outside the enum is an error, and the registry and schema are
compared in both directions.
Plus
yarn test:frameworks— 9 assertions pinning the membership of everyderived map, because the risk in this refactor is not a crash but a map quietly
gaining or losing an entry.
Separately
docs-gatenow skips prose checks for files whose body did not change. Themechanical frontmatter pass across 117 files dragged in 233 pre-existing Vale
findings (measured: 233 before, 233 after). Any file with a body edit is still
checked in full — verified by adding one sentence and watching both new findings
appear while the other 116 stayed skipped.
The ticket's test: a page set to
framework: netIosfails the gate beforenormalization and passes after — verified in both directions.