Skip to content

fix(docs): validate the singular framework field and collapse five framework maps into one - #430

Open
eugenia-scandit wants to merge 8 commits into
mainfrom
fix/framework-frontmatter-ssot
Open

fix(docs): validate the singular framework field and collapse five framework maps into one#430
eugenia-scandit wants to merge 8 commits into
mainfrom
fix/framework-frontmatter-ssot

Conversation

@eugenia-scandit

Copy link
Copy Markdown
Collaborator

Closes the ticket on the unvalidated framework field.

The problem

docs-schema.yml defined the plural frameworks array with an enum but did not
define the singular framework field at all. The schema allows undeclared
fields, 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

  • The enum in docs-schema.yml is the single source of truth; singular and
    plural share one vocabulary.
  • 117 pages normalized: 92 spelling drifts (netIos / netAndroid / react),
    and 25 express / bolt pages which were never frameworks — they live under
    docs/hosted/, so they are now framework: hosted with the value they carried
    moved to product:.
  • Five copies of the framework list replaced by one registry
    (src/constants/frameworks.ts), which UNRELEASED_FRAMEWORK_SLUGS also
    derives from.

Three things the ticket could not have known

  • kmp — 37 pages, more than any row in the ticket's table. Not drift: it is
    the 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
    kmp was added to it rather than the pages being rewritten.
  • FW_TO_PJ does not exist — PR feat: add ProductChooser ("Choose this if / not for" + framework matrix) #417 is unmerged. There were five copies,
    not three.
  • A live bug: FRAMEWORK_MAPPING had no linux entry, so
    parseSdksRoute() resolved no framework at all for every /sdks/linux/ page.

Why nobody ever saw an error

docs-gate is a ratchet over changed files. Those 37 pages had been violating
the plural enum for months and were never once reported. So
yarn verify:frameworks (in CI) runs over the whole corpus and over the
registry: 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 every
derived map, because the risk in this refactor is not a crash but a map quietly
gaining or losing an entry.

Separately

docs-gate now skips prose checks for files whose body did not change. The
mechanical 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: netIos fails the gate before
normalization and passes after — verified in both directions.

eugenia-scandit and others added 3 commits August 19, 2026 12:12
…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>
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://Scandit.github.io/data-capture-documentation/pr-preview/pr-430/

Built to branch gh-pages at 2026-08-24 13:01 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

eugenia-scandit and others added 5 commits August 24, 2026 10:35
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant