diff --git a/.changeset/agent-instruction-references.md b/.changeset/agent-instruction-references.md new file mode 100644 index 00000000000..99646bb8bf6 --- /dev/null +++ b/.changeset/agent-instruction-references.md @@ -0,0 +1,4 @@ +--- +--- + +Reorganize agent instructions into shared `references/agents` documents; documentation only. diff --git a/.github/skills/agentic-component-authoring/SKILL.md b/.github/skills/agentic-component-authoring/SKILL.md index d83ef87e6ce..69cfc808b5b 100644 --- a/.github/skills/agentic-component-authoring/SKILL.md +++ b/.github/skills/agentic-component-authoring/SKILL.md @@ -1,69 +1,74 @@ --- name: agentic-component-authoring -description: Generate or update Fluent UI React Native components in packages/agentic/components. Use for component APIs, slots, state hooks, token styling, render functions, tests, stories, and spec-driven component work. +description: Implement, change, or plan changes to components and primitives in packages/agentic/components, including every new component. Covers public APIs, slots, state, token styling, rendering, tests, and stories. Not for contract-only work, read-only reviews, v0/v1 maintenance, or the Storybook app toolchain. license: MIT --- # Agentic component authoring Build components in `packages/agentic/components` as React Native adaptations of the Fluent UI v9 component pattern. -This skill is the workflow router. Load only the references needed for the current change instead of placing every -authoring rule in one always-loaded instruction file. +New components always use this workflow; v0 and v1 are maintenance-only. This skill routes the work. Load only the +references the current change needs. ## Choose the component kind -- Higher-order components live in `src/components`. Read the - [higher-order component instructions](../../../packages/agentic/components/src/components/AGENTS.md). -- Primitive components live in `src/primitives`. Read the - [primitive instructions](../../../packages/agentic/components/src/primitives/AGENTS.md). -- If the change spans multiple components, repeated helpers, or shared dependency boundaries, read the - [package-wide optimization guidance](../agentic-component-optimization/SKILL.md) and - [`packages/agentic/components/src/AGENTS.md`](../../../packages/agentic/components/src/AGENTS.md). Do not run a - whole-package audit for a tiny single-component edit. -- Work on the Storybook application, native projects, bundling, or CocoaPods belongs to the - [agentic Storybook development skill](../agentic-storybook-development/SKILL.md), not this component workflow. +- **Higher-order components** live in `src/components`. Read the + [package instructions](../../../packages/agentic/components/AGENTS.md) and the + [higher-order component instructions](../../../packages/agentic/components/src/components/AGENTS.md). The + component's `SPEC.md` and `spec/` companions are its contract. +- **Primitives** live in `src/primitives`. Read the package instructions and the + [primitive instructions](../../../packages/agentic/components/src/primitives/AGENTS.md). A primitive's colocated + `CONTRACT.md` is its contract; primitives have no upstream-backed `SPEC.md`. +- Changes spanning components, repeated helpers, or dependency boundaries also use the + [optimization skill](../agentic-component-optimization/SKILL.md). Do not audit the whole package for a small edit. +- Read-only reviews use [review-component](../review-component/SKILL.md). Storybook application, native project, + bundling, or CocoaPods work uses the [Storybook skill](../agentic-storybook-development/SKILL.md). +- Native views, codegen, or registration also use the + [native component skill](../implement-native-component/SKILL.md) as an overlay. ## Load focused references -| Work | Reference | -| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| Source interpretation, provenance, contract drafting, review, or divergences | [Contract authoring](../agentic-component-contract-authoring/SKILL.md) | -| Public props, slots, state types, native prop exposure, or exports | [Types and slots](references/types-and-slots.md) | -| Defaults, derived state, interaction hooks, accessibility, or slots | [State and accessibility](references/state-and-accessibility.md) | -| Focus, focus visuals, keyboard activation, or native focus refs | [Common focus authoring](references/focus.md), then [Windows/Win32](references/focus-windows.md) or [macOS](references/focus-macos.md) | -| Tokens, theme caching, state precedence, or slot style application | [Styles and tokens](references/styles-and-tokens.md) | -| Pure slot rendering, component assembly, or display names | [Rendering and assembly](references/rendering.md) | -| Runtime tests, type tests, snapshots, Storybook stories, or validation | [Tests and stories](references/tests-and-stories.md) | -| Native React Native Windows Fabric components, codegen, registration, or UIA | [Windows Fabric native components](references/windows-fabric-native-components.md) | -| Cross-component duplication, shared extraction, or dependency hygiene | [Package optimization](../agentic-component-optimization/SKILL.md) | +| Work | Reference | +| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Source interpretation, provenance, contract drafting, or divergences | [Contract authoring](../agentic-component-contract-authoring/SKILL.md) | +| Public props, slots, state types, native prop exposure, or exports | [Types and slots](../../../references/agents/components/agentic/types-and-slots.md) | +| Defaults, derived state, interaction hooks, accessibility, or slots | [State and accessibility](../../../references/agents/components/agentic/state-and-accessibility.md) | +| Focus, focus visuals, keyboard activation, or native focus refs | [Common focus authoring](../../../references/agents/focus/common.md), then [Windows/Win32](../../../references/agents/focus/windows-win32.md) or [macOS](../../../references/agents/focus/macos.md) | +| Tokens, theme caching, state precedence, or slot style application | [Styles and tokens](../../../references/agents/components/agentic/styles-and-tokens.md) and [tokens and theming](../../../references/agents/design/tokens-and-theming.md) | +| Pure slot rendering, component assembly, or display names | [Rendering and assembly](../../../references/agents/components/agentic/rendering.md) | +| Runtime tests, type tests, snapshots, Storybook stories, or validation | [Tests and stories](../../../references/agents/components/agentic/tests-and-stories.md) | +| Native React Native Windows Fabric components, codegen, registration, or UIA | [Windows Fabric native components](../../../references/agents/native/windows-fabric.md) | -A new higher-order component normally needs every reference. A focused fix should load only the affected reference and -its immediate neighbors. Keep the component's colocated `SPEC.md` and companion files authoritative for its contract. +A new higher-order component normally needs the types, state, styles, rendering, and tests references, plus focus +guidance when it is focusable. Load the Windows Fabric reference only for native Windows views. A focused fix loads only +the affected reference and its immediate neighbors. ## Workflow -1. Read the repository and package instructions, the component `SPEC.md`, - `spec/source.json`, and every React Native companion referenced by the spec. -2. Use an existing reviewed contract directly when its source evidence and - requested behavior are unchanged. If the contract is absent, stale, still a - draft, or accompanied by new source evidence, run the - [contract authoring workflow](../agentic-component-contract-authoring/SKILL.md) - and complete its pre-code review gate first. -3. Inspect the closest canonical implementation. Use - [`components/button`](../../../packages/agentic/components/src/components/button) for a styled higher-order component - and [`primitives/icon`](../../../packages/agentic/components/src/primitives/icon) for a direct primitive. +For a plan-only request, follow steps 1 to 4 without editing, then return the planned contract revision, the +references used, the implementation order, and the validation plan. + +1. Read the package instructions and the component's contract: `SPEC.md`, `spec/source.json`, and every React Native + companion it references for a higher-order component, or `CONTRACT.md` for a primitive. +2. Higher-order components: use an existing reviewed contract directly when its source evidence and requested behavior + are unchanged. If the contract is absent, stale, still a draft, or accompanied by new source evidence, run the + [contract authoring workflow](../agentic-component-contract-authoring/SKILL.md) and complete its pre-code review + gate first (R-AGENTIC-001 in the [package instructions](../../../packages/agentic/components/AGENTS.md)). +3. Inspect the closest canonical implementation: + [`components/button`](../../../packages/agentic/components/src/components/button) for a styled higher-order + component, or [`primitives/icon`](../../../packages/agentic/components/src/primitives/icon) for a direct primitive. 4. Establish the public contract before implementation: variants, slots, native props, accessibility, interaction states, and platform behavior. 5. Implement in dependency order: types and slots, state and accessibility, styles and slot props, pure rendering, component assembly, and explicit exports. -6. Preserve the specification. Record a genuine token or platform gap rather than substituting an unrelated value or +6. Preserve the contract. Record a genuine token or platform gap rather than substituting an unrelated value or web-only behavior. 7. Add focused tests and stories that exercise the public API and the resolved native output. -8. Reconcile and ratify the local contract against the realized public types, - tests, stories, and platform evidence. Upstream changes require explicit - re-review and never overwrite the local contract. -9. Run the smallest declared validation command while iterating. Finish with package format, lint, build, and tests; run - the Storybook bundle for story changes and the root build when public types, manifests, or project references change. +8. Reconcile and ratify the local contract against the realized public types, tests, stories, and platform evidence. + Upstream changes require explicit re-review and never overwrite the local contract. +9. Run the smallest declared validation command while iterating. Finish with package format, lint, build, and tests; + run the Storybook bundle for story changes and the root build when public types, manifests, or project references + change. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003) and report results truthfully (R-VALID-002). Do not divide one component implementation into separate sub-agent or sub-skill phases. Its types, state, styling, and rendering form one contract and should remain in one implementation context. diff --git a/.github/skills/agentic-component-authoring/references/spec-source-adaptation.md b/.github/skills/agentic-component-authoring/references/spec-source-adaptation.md deleted file mode 100644 index 8e4139ba5b6..00000000000 --- a/.github/skills/agentic-component-authoring/references/spec-source-adaptation.md +++ /dev/null @@ -1,9 +0,0 @@ -# Flex source adaptation - -Contract creation now belongs to the -[agentic component contract authoring skill](../../agentic-component-contract-authoring/SKILL.md). -For pinned Flex component skills and X3 lineage, use its -[Flex and X3 source adapter](../../agentic-component-contract-authoring/references/sources/flex-x3.md). - -This compatibility reference remains so existing links continue to resolve. -Implementation guidance stays in the agentic component authoring skill. diff --git a/.github/skills/agentic-component-contract-authoring/SKILL.md b/.github/skills/agentic-component-contract-authoring/SKILL.md index 3a548b239b4..eb9c0a22b74 100644 --- a/.github/skills/agentic-component-contract-authoring/SKILL.md +++ b/.github/skills/agentic-component-contract-authoring/SKILL.md @@ -1,6 +1,6 @@ --- name: agentic-component-contract-authoring -description: Create or revise reviewed React Native component contracts in packages/agentic/components from Flex/X3, Fluent v9, HTML/CSS, images, local platform foundations, or composite evidence. +description: Create or revise reviewed React Native component contracts in packages/agentic/components from Flex/X3, Fluent v9, HTML/CSS, images, local platform foundations, or composite evidence. Use for contract-only or contract-feasibility work, and as the contract gate before implementing or planning a component change. license: MIT --- @@ -11,14 +11,15 @@ contract. This skill owns source interpretation, authority, provenance, uncertainty, React Native adaptation, divergences, and requirement planning. It does not own component implementation. -Use this skill directly for contract-only, feasibility, or implementation-plan -requests. The [component authoring skill](../agentic-component-authoring/SKILL.md) +Use this skill directly for contract-only or contract-feasibility requests. A +plan to change a component's implementation or public API starts in the +[component authoring skill](../agentic-component-authoring/SKILL.md), which invokes this workflow before code when a contract is missing, stale, still a draft, or accompanied by new source evidence. ## Choose the request mode -- **Feasibility or plan only:** inspect the evidence, identify blockers and +- **Feasibility or contract plan:** inspect the evidence, identify blockers and unknowns, and describe the contract work without changing implementation. Commit contract files only when the user requested a contract. - **Contract only:** create or revise the local contract and stop after its @@ -31,14 +32,14 @@ draft, or accompanied by new source evidence. Load every adapter represented by the supplied evidence, but no others. -| Evidence | Adapter | -| --------------------------------------------- | -------------------------------------------------------------- | -| Pinned Flex component skill and X3 lineage | [Flex and X3](references/sources/flex-x3.md) | -| Fluent UI v9 source or specification | [Fluent v9](references/sources/fluent-v9.md) | -| HTML, CSS, computed styles, or browser output | [HTML and CSS](references/sources/html-css.md) | -| Images, screenshots, or design renders | [Visual reference](references/sources/visual-reference.md) | -| Public native API plus repository precedent | [Local foundation](references/sources/local-foundation.md) | -| More than one of the preceding source sets | [Composite evidence](references/sources/composite-evidence.md) | +| Evidence | Adapter | +| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Pinned Flex component skill and X3 lineage | [Flex and X3](../../../references/agents/components/agentic/contract-sources/flex-x3.md) | +| Fluent UI v9 source or specification | [Fluent v9](../../../references/agents/components/agentic/contract-sources/fluent-v9.md) | +| HTML, CSS, computed styles, or browser output | [HTML and CSS](../../../references/agents/components/agentic/contract-sources/html-css.md) | +| Images, screenshots, or design renders | [Visual reference](../../../references/agents/components/agentic/contract-sources/visual-reference.md) | +| Public native API plus repository precedent | [Local foundation](../../../references/agents/components/agentic/contract-sources/local-foundation.md) | +| More than one of the preceding source sets | [Composite evidence](../../../references/agents/components/agentic/contract-sources/composite-evidence.md) | ## Workflow @@ -58,7 +59,9 @@ Load every adapter represented by the supplied evidence, but no others. 5. Draft the original React Native contract: - public API, slots, defaults, state ownership, and requirement IDs in `SPEC.md`; - - FURN token bindings and gaps in `spec/tokens.yaml`; + - FURN token bindings and gaps in `spec/tokens.yaml`, mapped through the + [Flex token map](../../../packages/agentic/design/src/tokens/mappings/flex-token-map.yaml). + Name each proposed binding or list it as unresolved; - native accessibility in `spec/accessibility.md`; - keyboard, pointer, focus, and motion behavior in `spec/interaction.md`; diff --git a/.github/skills/agentic-component-optimization/SKILL.md b/.github/skills/agentic-component-optimization/SKILL.md index 8ef5ebf9220..54a1a13240f 100644 --- a/.github/skills/agentic-component-optimization/SKILL.md +++ b/.github/skills/agentic-component-optimization/SKILL.md @@ -12,21 +12,20 @@ primitive or helper should exist at all. ## Read first -1. Read `packages/agentic/components/AGENTS.md` and `packages/agentic/components/src/AGENTS.md`. -2. Read the relevant component or primitive `AGENTS.md` file and the colocated `SPEC.md` files for the affected area. +1. Read the [package instructions](../../../packages/agentic/components/AGENTS.md) and the + [source instructions](../../../packages/agentic/components/src/AGENTS.md). +2. Read the relevant component or primitive `AGENTS.md` file and the colocated `SPEC.md` or `CONTRACT.md` files for the + affected area. 3. Inspect the canonical implementations: - - `packages/agentic/components/src/components/button` - - `packages/agentic/components/src/primitives/icon` + [`components/button`](../../../packages/agentic/components/src/components/button) and + [`primitives/icon`](../../../packages/agentic/components/src/primitives/icon). ## Audit checklist ### 1. Dependency direction -- Production source may depend only on React / React Native native components, `@fluentui-react-native/design`, and - `@fluentui-react-native/framework-base`. -- Generalizable non-styling hooks belong in `framework-base/src/hooks`. -- Styling helpers belong in `agentic/design/src/styling`. -- Component-library-specific non-public types, constants, and helpers belong in `packages/agentic/components/src/common`. +Apply the [source boundaries](../../../packages/agentic/components/src/AGENTS.md#source-boundaries): production +dependencies, where generalizable hooks and styling helpers belong, and what stays in `src/common`. ### 2. Repetition and ownership diff --git a/.github/skills/agentic-storybook-development/SKILL.md b/.github/skills/agentic-storybook-development/SKILL.md index 42d8ae2dcc3..de68596067a 100644 --- a/.github/skills/agentic-storybook-development/SKILL.md +++ b/.github/skills/agentic-storybook-development/SKILL.md @@ -12,7 +12,7 @@ Work on the native Storybook application in `apps/storybook`. 1. Read the repository `AGENTS.md`, then the compact [agent map](../../../apps/storybook/agent-map.yaml). Read the - [Storybook instructions](../../../apps/storybook/AGENTS.md) and + [Storybook instructions](../../../apps/storybook/AGENTS.md), including its native diagnostics commands, and [README](../../../apps/storybook/README.md) only as needed for the task. 2. Inspect the Storybook `package.json` and run its declared workspace scripts; do not invent direct runner commands. 3. Use `storybook-agent` to query runtime story IDs, select stories, and sweep renderability. Reproduce the first @@ -25,7 +25,7 @@ Work on the native Storybook application in `apps/storybook`. spec without refreshing ReactCodegen's source list. If the clean build then reports a missing generated spec header, run `pods:macos` once more before changing native configuration. 6. Validate the JavaScript bundle and the affected native build. Run Storybook format/lint and the root build when - manifests or shared package references changed. + manifests or shared package references changed. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). Keep component API, slot, token, and rendering work in the `agentic-component-authoring` skill; use this skill for the Storybook application and its native toolchains. diff --git a/.github/skills/fix-security-issues/SKILL.md b/.github/skills/fix-security-issues/SKILL.md index 998d322e736..075503cca77 100644 --- a/.github/skills/fix-security-issues/SKILL.md +++ b/.github/skills/fix-security-issues/SKILL.md @@ -7,157 +7,40 @@ license: MIT # Fix security issues Use this skill for dependency vulnerability remediation, Dependabot alerts, -Yarn audit findings, or security-driven package upgrades. +Yarn audit findings, or security-driven package upgrades. Detailed commands and +procedures are in the [dependency security reference](../../../references/agents/repository/dependency-security.md); read each linked +section when you reach that step. -## Read first +## Safeguards -1. Read the repository `AGENTS.md` and the owning workspaces' instructions. -2. Inspect `package.json`, `.yarnrc.yml`, `yarn.lock`, package catalogs, - resolutions, package extensions, and dynamic package extensions. -3. Inspect `.github/dependabot.yml` and any repository-specific package age, - cooldown, registry, or release policy. -4. Inspect the declared scripts for every affected workspace before choosing - validation commands. - -Do not edit `yarn.lock` manually. Change the owning manifest, catalog, -resolution, preset, or extension and let Yarn regenerate the graph. - -## Inventory the complete problem - -Start with the full recursive audit rather than a partial summary: - -```bash -yarn npm audit --all --recursive --json --no-deprecations -``` - -Normalize the output by severity, package, installed version, advisory, and -dependent. For every finding: - -1. Read the advisory and identify the first patched version, affected - functions, exploit preconditions, and whether a patch exists. -2. Run `yarn why ` to find every path. A fixed direct path does not - help if a stale compatible transitive path remains. -3. Search repository source and installed dependent source to determine - whether the vulnerable capability is used at runtime, only used by tooling, - or unused. -4. Group duplicate advisories for the same package, but do not omit findings - because one report calls them low priority. - -Treat critical and high findings as required fixes. Address moderate and low -findings whenever a compatible, policy-eligible remediation exists. - -## Enforce the package age policy - -Before resolving versions, inspect the effective Yarn policy: - -```bash -yarn config get npmMinimalAgeGate -yarn config get npmPreapprovedPackages -``` - -Calculate the exact cutoff from the session time. Verify publication timestamps -for proposed versions and, after lockfile generation, every newly selected npm -version in the lockfile diff. - -- Do not disable or reduce the age gate to obtain a security fix. -- Do not assume `latest` is eligible. -- Check preapproved-package exceptions explicitly; an exception does not prove - that a newly selected release meets the requested age policy. -- Prefer the newest eligible fixed patch within the existing compatible range, - not the newest package release overall. - -## Choose the least risky complete remediation - -Use this order: - -1. **Upgrade the direct dependency.** Prefer a patched version within the - current major and update the shared catalog when it is the source of truth. -2. **Upgrade the transitive parent.** This is preferable when the parent has - already adopted the fixed dependency. -3. **Add a narrow root resolution.** Use the exact vulnerable descriptor when - possible. Review API and module-format compatibility before forcing a new - major, and exercise the dependent package's real code path. -4. **Remove an unused vulnerable capability with `ignore:`.** Use the installed - `@rnx-kit/yarn-plugin-ignore` only after proving the package is not required - by repository workflows. -5. **Carry a local Yarn patch.** Reserve this for required packages with no - eligible published fix. Base it on authoritative upstream work, add a - focused regression check, and document that version-based scanners may - still report the patched version. - -Do not add a broad catch, audit exclusion, or warning suppression in place of a -runtime fix. - -## Safely use the ignore plugin +- **R-SEC-001:** Do not disable or reduce the age gate to obtain a security fix. +- **R-SEC-002:** Do not add a broad catch, audit exclusion, or warning suppression in place of a runtime fix. +- Do not edit `yarn.lock` manually. Change the owning manifest, catalog, + resolution, preset, or extension and let Yarn regenerate the graph. +- Treat critical and high findings as required fixes. Address moderate and low + findings whenever a compatible, policy-eligible remediation exists. +- Use `ignore:` only for a capability proven unused that fails closed; never + ignore a package merely to make the audit output clean. -The repository's `ignore:` protocol replaces a package with an empty module. -It is appropriate when a transitive dependency implements an optional -capability that this repository intentionally does not support, such as a -browser downloader in a native-only test path. +## Workflow -Before adding a resolution such as: - -```json -{ - "resolutions": { - "unused-vulnerable-package": "ignore:" - } -} -``` - -prove all of the following: - -- no repository source imports or invokes the package; -- each dependent imports it lazily or can load with an empty module; -- the unsupported capability fails closed if invoked; -- declared builds, tests, bundles, and CLIs do not require it; -- `yarn why` shows only the understood optional paths. - -After installation, verify the npm package is absent from `yarn.lock`, the -`ignore:` locator is present, dependent modules still load, and an attempted -call cannot silently succeed. Never ignore a package merely to make the audit -output clean. - -## Resolve, audit, and deduplicate iteratively - -After each coherent manifest change: - -```bash -yarn install -yarn npm audit --all --recursive --json --no-deprecations -yarn why -``` - -Yarn may retain an older resolution that still satisfies a transitive range, -even when a fixed version is also installed. Once the remediation set is -complete, run: - -```bash -yarn dedupe --strategy highest -yarn dedupe --check -yarn install --immutable --mode=skip-build -``` - -Re-run the audit after dedupe. If stale vulnerable paths remain, update their -owning range or add a narrow compatible resolution; do not assume another -dedupe pass will update a package with only one descriptor. - -Keep related package families aligned. If repository lint expects an older -vulnerable version, update the shared alignment preset or catalog rather than -scattering exceptions or reverting the security floor. - -## Validate the affected behavior - -Use declared workspace scripts and validate in increasing scope: - -1. Format changed manifests and configuration. -2. Run lockfile and repository structural lint. -3. Run affected workspace lint, build, and tests. -4. Exercise the runtime path affected by every major-version resolution. -5. Bundle each affected Metro/native target when bundler dependencies change. -6. Run the root build and repository test graph when shared manifests, - catalogs, resolutions, or the lockfile change broadly. -7. Add a normal or empty changeset as required by repository policy. +1. Read the repository `AGENTS.md` and the owning workspaces' instructions. + Inspect `package.json`, `.yarnrc.yml`, `yarn.lock`, package catalogs, + resolutions, package extensions, dynamic package extensions, + `.github/dependabot.yml`, any repository-specific package age, cooldown, + registry, or release policy, and the declared scripts for every affected + workspace before choosing validation commands. +2. [Inventory the complete problem](../../../references/agents/repository/dependency-security.md#inventory-the-complete-problem) with + the full recursive audit, `yarn why`, and runtime-use analysis. +3. [Enforce the package age policy](../../../references/agents/repository/dependency-security.md#enforce-the-package-age-policy) with + an exact cutoff for every newly selected version. +4. [Choose the least risky complete remediation](../../../references/agents/repository/dependency-security.md#choose-the-least-risky-complete-remediation): + direct dependency, transitive parent, narrow root resolution, + [`ignore:`](../../../references/agents/repository/dependency-security.md#safely-use-the-ignore-plugin), then a local Yarn patch. +5. [Resolve, audit, and deduplicate iteratively](../../../references/agents/repository/dependency-security.md#resolve-audit-and-deduplicate-iteratively). +6. [Validate the affected behavior](../../../references/agents/repository/dependency-security.md#validate-the-affected-behavior), add a + normal or empty changeset as required by repository policy, and finish with + `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). Finish by confirming: @@ -175,11 +58,3 @@ ignored, and cannot be patched with a focused regression test, report the remaining advisory and its exposure plainly. Do not claim completion, weaken the age policy, force an unvalidated incompatible major, or suppress the warning. - -## Example - -If `extract-zip` is present only through optional browser installation code -while the repository runs native Appium automation, inspect both dependents, -confirm their normal modules load without extraction, resolve `extract-zip` to -`ignore:`, regenerate and deduplicate the lockfile, then prove the browser -archive path fails closed and the native tooling still works. diff --git a/.github/skills/implement-native-component/SKILL.md b/.github/skills/implement-native-component/SKILL.md new file mode 100644 index 00000000000..3e9beaa6941 --- /dev/null +++ b/.github/skills/implement-native-component/SKILL.md @@ -0,0 +1,73 @@ +--- +name: implement-native-component +description: Implement or change native components, TurboModules, native services, codegen specs, or native-backed platform wrappers (Swift, Objective-C, C++/WinRT, podspecs, Windows projects). Also supplies the native overlay checklist for v0, v1, and agentic work. Not for Storybook toolchain failures. +license: MIT +--- + +# Implement native components and modules + +Use this skill directly for native work, or as an overlay when a v0, v1, or agentic change touches a native boundary. +The [native components and modules reference](../../../references/agents/native/components-and-modules.md) defines +ownership and qualification rules (R-NATIVE-001 through R-NATIVE-003). + +## Workflow + +1. Classify the work: + - native kind: view component, TurboModule, or native service; + - target platforms and renderers: macOS Paper or Fabric, React Native Windows Fabric, Win32 host-provided views, + iOS, or Android; + - the JavaScript wrapper's generation and public compatibility, using the + [classification guide](../../../references/agents/components/classification.md); + - native registration names and entrypoints that must not change. +2. Choose ownership. New modern native code belongs in native-core (R-NATIVE-001); maintain existing native wrappers, + such as experimental packages, where they live. For native-core work, read the relevant sections of the + [native-core README](../../../packages/agentic/native-core/README.md); for a wrapper in another package, read that + package's sources and documentation instead. +3. Load only the platform references the work needs: + - React Native Windows Fabric views: [Windows Fabric](../../../references/agents/native/windows-fabric.md); + - any iOS or macOS Swift, Objective-C, header, podspec, or bridge-manager change: + [Apple native authoring and Office integration](../../../references/agents/native/apple-office.md) + (R-APPLE-001 through R-APPLE-007), plus native-core's macOS layout notes for native-core work; + - focus targets or native focus visuals: [common focus authoring](../../../references/agents/focus/common.md) and its + platform file; + - accessibility actions: + [framework-base accessibility instructions](../../../packages/framework-base/src/accessibility/AGENTS.md); + - the JavaScript wrapper: the [agentic skill](../agentic-component-authoring/SKILL.md), + [v0 recipe](../../../references/agents/components/v0.md), or [v1 recipe](../../../references/agents/components/v1.md). +4. Implement with the TypeScript specification, codegen configuration, native props, events, commands, and registration + kept consistent. Preserve public APIs and registration names (R-NATIVE-002) and keep fork imports in platform files + (R-REPO-002). +5. Validate the affected boundary with declared scripts (R-VALID-001): package build, test, and lint; native-core + `codegen:windows` when Windows specs change; native builds through the owning app, such as + `yarn storybook build --macos` or `--windows` from `apps/storybook`; and runtime evidence through Storybook smoke or + `wdio` story tests. macOS Storybook commands default to Fabric: add `--paper` to `prep`, `build`, `run`, or `smoke` + for Paper, and rerun `prep` when switching renderers. Desktop Storybook does not build iOS: validate iOS-only changes + through the owning iOS host, such as FluentTester's iOS app in `apps/fluent-tester`. Apple changes also need the module and bridging-header checks in + R-APPLE-007. Run Pod operations only as the + [permissions reference](../../../references/agents/repository/permissions.md) allows. +6. Report each platform and renderer as passed, failed, skipped, not run, or unsupported, with reasons (R-NATIVE-003). +7. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). + +Storybook application or toolchain failures that are not caused by the native change belong to the +[Storybook skill](../agentic-storybook-development/SKILL.md). + +## Native overlay checklist + +Use this checklist for implementation and review: + +- TypeScript specification, native props, events, and commands agree; generated code was regenerated by its declared + command rather than edited by hand. +- Registration names, autolinking, and entrypoints are unchanged unless requested; consuming apps depend directly on + native-core. +- Native view lookup and UI mutation run on the platform's UI thread. +- iOS and macOS changes follow the [Apple and Office rules](../../../references/agents/native/apple-office.md): + qualified `` headers, guarded Swift `import React`, no generated Swift header in an `RCT_EXTERN_MODULE` + file, generated-type-free Paper bridges, main-queue dispatch, and strict warnings (R-APPLE-001 through R-APPLE-006). + Module and bridging-header validation ran, or the unrun mode is reported (R-APPLE-007). +- Native resources, observers, and pending work are released on unmount, cancellation, or failure. +- Each export works on every host its entrypoint promises; platform-only APIs use the narrowest entrypoint. +- Podspecs, Windows projects, and package `files` include every new native source. +- Accessibility actions and focus targets are qualified by invoking the native action or target, not by JavaScript + events alone. +- Evidence states which platforms and renderers ran; JavaScript-only results are not native qualification + (R-NATIVE-003). diff --git a/.github/skills/maintain-v0-v1-component/SKILL.md b/.github/skills/maintain-v0-v1-component/SKILL.md new file mode 100644 index 00000000000..07ea03fcc01 --- /dev/null +++ b/.github/skills/maintain-v0-v1-component/SKILL.md @@ -0,0 +1,39 @@ +--- +name: maintain-v0-v1-component +description: Fix or maintain existing v0 (foundation-compose) or v1 (framework compose/compressible) components, including their platform variants. Classifies the generation first and loads only its recipe. Not for new components, agentic components, read-only reviews, or native-only changes. +license: MIT +--- + +# Maintain v0 and v1 components + +v0 and v1 are maintenance-only (R-V0-001 and R-V1-001). A request for a new component uses the +[agentic authoring skill](../agentic-component-authoring/SKILL.md). A read-only review uses +[review-component](../review-component/SKILL.md). + +## Workflow + +1. Classify every surface the change touches with the + [classification guide](../../../references/agents/components/classification.md). Confirm the generation from + imports and structure, not directory names (R-CLASSIFY-001); Button contains both generations. A legacy public + entrypoint is not evidence of v0 or v1: native-core's `/legacy` wrappers use framework-base patterns and belong to + the [native component skill](../implement-native-component/SKILL.md). +2. Load only the matching recipe: [v0](../../../references/agents/components/v0.md) or + [v1](../../../references/agents/components/v1.md). Load both only when the change touches both generations. +3. Add overlays only when the change triggers them: + - native root, registration, codegen, podspec, or native project: the + [native component skill](../implement-native-component/SKILL.md) checklist; + - focus, keyboard activation, or focus visuals: [common focus authoring](../../../references/agents/focus/common.md) + and its platform file; + - token values or theme behavior: [tokens and theming](../../../references/agents/design/tokens-and-theming.md); + - FluentTester pages or E2E specs: [legacy test apps](../../../references/agents/components/legacy-test-apps.md). +4. Read the component's existing tests, snapshots, platform variants, and any `SPEC.md` or `MIGRATION.md` before + editing. +5. Make the smallest change that fixes the problem. Preserve public props, types, exports and aliases, display names, + `componentRef`, token contracts, and native registration names (R-V0-002, R-V0-003, R-V1-002, R-V1-003). +6. Validate with the owning package's declared scripts (R-VALID-001): build, test, and lint, plus the framework + packages named in the recipe when framework semantics change. Review snapshot diffs before accepting them. Report + each check's outcome (R-VALID-002), and finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). +7. Add a changeset for published packages (R-REPO-003). + +Stop and confirm scope before migrating between frameworks, creating a component, or changing a public API. Those +are separate tasks, not maintenance. diff --git a/.github/skills/review-component/SKILL.md b/.github/skills/review-component/SKILL.md new file mode 100644 index 00000000000..40c6a91044a --- /dev/null +++ b/.github/skills/review-component/SKILL.md @@ -0,0 +1,46 @@ +--- +name: review-component +description: Review component code or changes of any generation (v0, v1, agentic, primitive, or native-backed) without editing files. Classifies each changed surface, applies the matching checklists and native overlay, and reports evidence-backed findings with validation status. +license: MIT +--- + +# Review a component change + +- **R-REVIEW-001:** A review is read-only. Do not edit files, update snapshots, commit, or invoke an implementation + workflow unless the user asks for fixes after receiving the review. +- **R-REVIEW-002:** Report only evidence-backed findings. Cite `path:line`, the violated rule ID or contract + requirement ID, and whether the finding was verified (executed or confirmed in source) or inferred. + +## Workflow + +1. Establish the scope: the named files, the working-tree diff, or the branch diff against its base. +2. Classify each changed surface with the + [classification guide](../../../references/agents/components/classification.md): task intent, implementation model, + public compatibility, and native obligations (R-CLASSIFY-001, R-CLASSIFY-002). +3. Load only the checklists for the surfaces present: + + | Surface | Checklist | + | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | v0 | [v0 review checklist](../../../references/agents/components/v0.md#review-checklist) | + | v1 | [v1 review checklist](../../../references/agents/components/v1.md#review-checklist) | + | Agentic higher-order | [Package](../../../packages/agentic/components/AGENTS.md) and [higher-order](../../../packages/agentic/components/src/components/AGENTS.md) invariants; the component's `SPEC.md` and `spec/`; and the review checklists in [types and slots](../../../references/agents/components/agentic/types-and-slots.md#review-checklist), [state and accessibility](../../../references/agents/components/agentic/state-and-accessibility.md#review-checklist), [styles and tokens](../../../references/agents/components/agentic/styles-and-tokens.md#review-checklist), and [rendering](../../../references/agents/components/agentic/rendering.md#review-checklist) for the areas changed | + | Agentic primitive | [Primitive invariants](../../../packages/agentic/components/src/primitives/AGENTS.md), the primitive's `CONTRACT.md`, and [primitive contracts](../../../references/agents/components/agentic/types-and-slots.md#primitive-contracts) | + | Tests and stories | [Tests and stories](../../../references/agents/components/agentic/tests-and-stories.md) for agentic code; [legacy test apps](../../../references/agents/components/legacy-test-apps.md) for v0 and v1 | + | Contract or provenance | [Contract authoring](../agentic-component-contract-authoring/SKILL.md) rules and only the [source adapters](../../../references/agents/README.md#document-index) matching the changed evidence | + | Focus or keyboard | [Required focus evidence](../../../references/agents/focus/common.md#required-evidence-before-shipping) and the affected platform file | + | Native code or roots | [Native overlay checklist](../implement-native-component/SKILL.md#native-overlay-checklist); for iOS or macOS, also [Apple and Office rules](../../../references/agents/native/apple-office.md) | + | Framework-base pattern | [Framework-base instructions](../../../packages/framework-base/AGENTS.md) | + +4. Check repository-wide rules: explicit exports (R-REPO-001), platform fork imports (R-REPO-002), changesets + (R-REPO-003), and no copied private source (R-REPO-004). +5. Inspect validation claims. Run non-mutating checks only when they add evidence; never run snapshot-update or fix + modes. Record each check as passed, failed, skipped, not run, or unsupported (R-VALID-002). + +## Output + +1. **Scope and classification:** one line per changed surface. +2. **Findings**, highest severity first. For each: `path:line`, the problem, the rule or requirement ID, verified or + inferred, and a suggested correction. +3. **Validation:** commands run with results, and checks not run with the reason. + +When nothing qualifies, say so and list what was checked. Do not pad the review with style preferences. diff --git a/AGENTS.md b/AGENTS.md index fe2e40686aa..2a4d2268713 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,385 +1,84 @@ # AGENTS.md -This file provides guidance to coding agents (Claude Code and others) when working with code in this repository. - -## Agent permissions - -The shared Copilot CLI defaults are implemented by -[the repository hook](.github/hooks/agent-permissions.json), not by prose instructions alone. -After trusting this repository, start a new CLI session to load the hook. Node.js must be on `PATH`; -no dependency installation is needed for the hook itself. - -- Read and write files inside this repository. -- Run all Yarn commands, including `yarn` and `yarn install`. -- Inspect Git state, stage changes, create new local commits, list branches, and fetch from `origin`. - Do not amend commits, discard other people's changes, or rewrite history without explicit authorization. -- Read GitHub issues, pull requests, workflow definitions, and pipeline runs using `gh`. - REST `gh api` calls to user, repository, and search endpoints are approved only for GET requests, - without request-body/field flags. GraphQL and API mutations require separate approval. -- Inspect the active account with `gh auth status` and `gh api user --jq .login`, and use - `gh auth switch` to select an existing authenticated account when needed. Never print authentication tokens. -- Run `pod install` (including `--repo-update` and `--clean-install`) or named `pod update ...` operations, - optionally through `bundle exec`, from the owning app or its `ios`/`macos` directory. A project-directory - argument must resolve to that app's native directory and existing Podfile. Root-level Pod invocations, - updates of every pod, global cache cleanup, and `pod deintegrate` require separate approval. -- Inspect processes/listeners with constrained `ps` and `lsof` commands in Bash. Probe loopback `/status` and - `/index.json` using `curl -q` (disable user curl configuration), GET/HEAD, and the instance's reported port. - Uploads, redirects, output files, arbitrary endpoints, and process termination are not pre-approved. - -The hook accepts simple Bash/PowerShell commands and `&&` sequences when **every** command is approved. -`cd ./ && ...` is allowed only within the repository (use `./` to avoid Bash `CDPATH`). -In Bash, native-driver variables listed in the hook may prefix Yarn commands, directly for unquoted values -or through `env`, for example `FURN_NATIVE_DRIVER_TEST=1 yarn test` or -`env "FURN_DESKTOP_DRIVER_MACOS_SIGNING_IDENTITY=FURN Development" yarn storybook build-driver --macos`. -Arbitrary environment overrides, pipelines, redirections, shell expansions, unrecognized -options, remote writes, and destructive Git operations fall through to the CLI's normal permission handling; -they are not blanket-denied. Run commands separately when practical. File approvals resolve symlinks and -are limited to this repository. These defaults are **not a sandbox**: Yarn scripts and Git hooks can execute -arbitrary code, and explicit approvals intentionally trust them. - -### Native Storybook diagnostics - -The existing Yarn approval covers the supported macOS, Windows, and Win32 workflows; extra blanket approvals -for Xcode, Swift, PowerShell scripts, Metro, or screen capture are unnecessary. From `apps/storybook`: - -| Need | Preferred command | -| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Project generation and Pod installation | `yarn storybook prep --macos` | -| Bundle failures | `yarn storybook --verbose bundle --` | -| Native build failures | `yarn storybook --verbose build --` (not Win32) | -| Driver identity, signing, and macOS privacy diagnostics | `yarn desktop-driver doctor --platform macos --permissions` | -| Actual listener ports and target identity | `yarn storybook instance --` | -| Target-scoped screenshots | `yarn desktop-driver agent screenshot --platform --url --target --artifacts artifacts/ --name ` | -| Runtime trees and crash reproduction | `yarn desktop-driver agent describe ...` and `yarn storybook --verbose smoke --` | - -For `agent` commands, use `--platform macos` on macOS and `--platform windows` for both Windows and Win32. -Inspect retained command logs under `artifacts/storybook-commands` and platform artifacts rather than redirecting -output to arbitrary locations. Screenshots require a running driver/app and the operating system's own screen -recording permission; CLI approval does not grant Accessibility or Screen Recording access. -Keep evidence scoped to the registered target and ignored artifact directories. Unrestricted `screencapture`, -`osascript`, debugger attachment, external crash-report directories, certificate trust changes, and TCC resets -remain separately authorized. Do not grant these broadly to work around a failed native capability check. - -Contributors can add permissions through the CLI's "don't ask again in this repo" prompt, which saves -repo-scoped approvals in their local `~/.copilot/permissions-config.json`, or through `--allow-tool` flags. -Additional personal hooks can be defined in the gitignored `.github/copilot/settings.local.json`. -To opt out of hooks locally, set `disableAllHooks: true` there (this disables all non-policy hooks). -Do not commit personal permission files, account names, or credentials. Hook approvals precede normal -tool permission checks; disable the hook if you need CLI deny rules to govern these approved operations. - -See the [Copilot hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference) and -[saved permissions schema](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference#permissions-configjson). -Run `yarn test:agent-permissions` after changing the policy. - -## Project Overview - -This is the **FluentUI React Native** repository, a monorepo containing React Native components that implement Microsoft's Fluent Design System. The repository supports multiple platforms including iOS, Android, macOS, Windows, and Win32. - -## Repository Architecture - -### High-Level Structure - -``` -/apps/ - Demo and test applications - /fluent-tester/ - Main test app for component development - /E2E/ - End-to-end testing setup using Appium/WebDriverIO - /win32/ - Win32-specific test app - /component-generator/ - Tool to generate new components -/packages/ - Core library packages - /agentic/native-core/ - Shared modern native components and TurboModules - /components/ - UI component implementations (Button, Checkbox, Avatar, etc.) - /shim/ - Compatibility entrypoints (Callout, FocusZone, theming, etc.) - /framework/ - Core theming and composition framework - /composition/ - Component composition factory (current approach) - /theme/ - Theme system - /use-tokens/ - Token-based styling hooks - /use-slots/ - Slot-based component composition - /theming/ - Theme definitions for different platforms - /android-theme/ - /apple-theme/ - /default-theme/ - /win32-theme/ - /experimental/ - Components under active development - /deprecated/ - Old framework code (foundation-compose, foundation-composable) - /utils/ - Shared utilities and tools -/scripts/ - Build and development scripts (fluentui-scripts CLI) -/docs/ - Component and theming documentation -``` - -### Key Framework Concepts - -**Composition Framework**: The repository uses `@fluentui-react-native/composition` (located at `packages/framework/composition/`) for building components. This is the current approach and is simpler than the older foundation-compose/foundation-composable frameworks in `/deprecated/`. - -**Slots**: The slot pattern is used to compose higher-order components. A slot represents an inner component (actual entry in the render tree). For example, a Button might have slots for `root`, `icon`, and `content`. This allows advanced customization scenarios. Components wrapping a single native component typically have one slot. - -**Tokens**: Design tokens handle styling and customization. Tokens are design-time values set via theme or component customization (e.g., "brandColor"). Tokens can also be props (specified via "TokensThatAreAlsoProps"). This system enables simpler customization and better memoization. For Flex token authoring, use `packages/agentic/design/src/tokens/mappings/flex-token-map.yaml` as the canonical mapping from generic CSS and Fluent token sources to grouped React Native Flex token paths. When mapping an agentic component to an existing V1 component, use the Win32, macOS, or Windows implementation as the canonical compatibility reference; do not use iOS as the cross-platform canonical value unless the work explicitly targets iOS. - -**Color utilities**: Runtime-safe parsing, conversion, compositing, contrast, and interaction generation live behind the opt-in `@fluentui-react-native/design/color-lib` subpath. Keep its public exports intentionally narrow and add APIs only for demonstrated production needs. Color validation policy, semantic pairings, audit helpers, and conformance fixtures belong under `packages/agentic/design/src/testing/` and are exported, when generally useful to tests, from `@fluentui-react-native/design/testing`. Production code must not depend on the testing subpath, and testing-only validation does not need bundle-size scenarios. - -**Theme-specific styles**: Cache `StyleSheet.create` results that depend only on `ThemeState` with a module-scoped getter created by `themedStyleSheetFactory`. Treat them as immutable, and apply props, interaction state, and user styles separately so the cached sheet is safe to share between component instances. - -**Platform-Specific Files**: Components use platform-specific files with extensions like `.ios.ts`, `.android.ts`, `.win32.ts`, `.macos.ts` for platform-specific implementations. - -**Legacy vs V1**: Many components have both legacy and V1 implementations (e.g., `Button` and `ButtonV1`). The V1 versions use the newer composition framework and are preferred. - -## Build System & Commands - -The repository builds as a **single unified TypeScript project-references graph** compiled by **tsc** (`typescript`). The root `tsconfig.json` lists every package's `tsconfig.json` under `references`, and `yarn build` runs `tsc -b` over that graph, building each package in dependency order. **Lage** orchestrates the non-build tasks (bundle, test, lint, clean), and **Yarn 4 (Berry)** in **pnpm mode** manages dependencies (configured in `.yarnrc.yml`). - -### Primary Commands (from the repo root) - -```bash -yarn build # Unified TypeScript build of all packages (tsc -b); emits each package's lib/ -yarn build --clean # Remove build outputs (tsc -b --clean); follow with `yarn build` for a full rebuild -yarn lage test # Build + run tests across packages -yarn lage lint # Lint across packages -yarn lint-fix # Lint with autofix (cross-env FURN_FIX_MODE=true lage lint) -yarn bundle:repo # Bundle all packages (lage bundle) -yarn lage buildci # CI aggregate graph: lint-repo, check-publishing, build, test, lint -yarn clean # Clean build artifacts (lage clean) -yarn format # Format code with oxfmt -yarn format:check # Check formatting without writing -``` - -### Repo-Level Checks & Housekeeping - -```bash -yarn lint-repo # Repo-wide structural lint (scripts/src/tasks/lintRepo.ts) -yarn lint-lockfile # Validate the Yarn lockfile -yarn lage test-links # Validate links in repository Markdown files -yarn check-publishing # Validate package publishing configuration -yarn change # Create a change file for the current branch -yarn change:check # Verify required change files exist -yarn docs # Start the documentation site -``` - -### Lage Configuration - -The task pipeline is defined in `lage.config.mjs`: - -- Tasks declare dependency ordering (e.g. `test` dependsOn `build`) -- `buildci` is the aggregate CI alias (lint-repo, check-publishing, build, test, lint) -- Lage caches task outputs; add `--no-cache` to bypass caching and `--verbose` for detailed output -- After a major rework (e.g. moving packages, large refactors, or renaming exports), run `yarn lage test --no-cache` from the root once to force every test to re-run without relying on stale cached results. This also resets the Lage cache, so subsequent plain `yarn lage test` runs will work incrementally again. -- Before checking in moved or renamed files, run `yarn lage test-links` and update any repository links that still reference the old paths. - -### Package-Level Commands - -Each package's own `build` script is `tsc -b` (it builds that package together with its referenced dependencies). Other per-package tasks run through the `fluentui-scripts` CLI (in `/scripts/`): - -- `yarn build` - tsc project-references build (emits to `lib/`) -- `yarn lint` - ESLint (`fluentui-scripts lint`) -- `yarn test` - Jest tests where present (`fluentui-scripts jest`) -- `yarn depcheck` - Unused-dependency check (`fluentui-scripts depcheck`) -- `yarn format` / `yarn format:fix` - Check / fix formatting - -`fluentui-scripts` also retains a `build` command that can emit dual ESM (`lib/`) and CommonJS (`lib-commonjs/`) output driven by per-package build config, but packages on this branch build with `tsc -b` directly. - -### Agent validation and investigation discipline - -- Run commands through the owning workspace's declared scripts. Inspect its `package.json` before invoking an underlying - runner directly; do not assume scripts such as `jest` or `check` exist, and do not bypass project references with - ad-hoc `tsc` file arguments. -- Run working-directory-sensitive native tools from the owning app workspace. Flags such as CocoaPods' - `--project-directory` select project files but do not necessarily change subprocess resolution roots. -- During iteration, use the smallest relevant test and run package lint/build early after type-heavy edits. For final - validation, run format, lint, package build, package tests, then the root build when public types, manifests, or project - references changed. -- After fixing a failed validation step, rerun that step and its downstream checks. Avoid repeatedly running the entire - validation chain before the immediate failure is resolved. -- Prefer committed `*.types.test.ts` coverage for compile-time contracts. Keep exploratory probes outside package source, - or remove them before running package validation. -- Review small, localized diffs directly. Use an independent review agent only after focused validation passes and only - when the change still has a genuinely ambiguous or cross-cutting contract. -- Keep sessions scoped to one reviewable outcome. After capturing a durable result, start unrelated work in a fresh - session so stale context and generated output do not dominate later investigation. - -## TypeScript Configuration - -The repo uses **TypeScript 7.0.2** via **tsc**, which is automatically added as a dev dependency to every package that has a `tsconfig.json` through dynamic package extensions (`scripts/dynamic.extensions.mts`). - -The whole repo compiles as one **composite project-references graph**: the root `tsconfig.json` lists every package under `references`, `tsc -b` builds them in dependency order, and each package caches incremental state in `.cache/tsconfig.tsbuildinfo`. - -### Key TypeScript Settings - -- Base config: `scripts/configs/tsconfig/tsconfig.json` → extends `tsconfig.strict.json` → `@rnx-kit/tsconfig/tsconfig.nodenext.json` -- `target` / `lib`: `esnext` -- `module`: `esnext`, `moduleResolution`: `bundler` (ESM) -- `jsx`: `react-jsx` -- `rewriteRelativeImportExtensions: true` (ESM relative-import extension rewriting) -- `composite: true` (required for project references), `outDir: lib` -- Strict mode, relaxed for legacy compatibility (`strictNullChecks`, `noImplicitAny`, and `strictBindCallApply` are disabled in the base config) - -### Compatibility Notes - -- `suppressImplicitAnyIndexErrors` was removed in TS 5.8+ -- `Platform.OS` doesn't include `'win32'` in the React Native types even though react-native-windows supports it at runtime — use `Platform.OS === ('win32' as any)` -- The platform React Native forks (`react-native`, `react-native-windows`, `react-native-macos`, `@office-iss/react-native-win32`) have overlapping but divergent type definitions. Do **not** import more than one fork into a single program's type graph (it produces order-dependent type confusion under the unified build). Keep fork imports in `.win32.ts` / `.windows.ts` / `.macos.ts` files, or redeclare the needed shapes platform-neutrally. - -### Framework Type System - -The composition framework uses precise types for better type safety: - -- **`SlotFn`**: Slot functions return `React.ReactElement | null` (not `ReactNode`) - - This reflects the actual behavior: slots always return elements via staged render or `React.createElement` - - Provides better type inference when accessing slot props (e.g., `Slots.root({}).props`) -- **`FinalRender`**: Final render functions in staged components return `JSX.Element | null` - - Used in composition framework's `useRender` functions - - Ensures type compatibility between staged components and the composition system - -## Development Workflow - -### Setting Up Development Environment - -1. Clone repository -2. Run `yarn` to install dependencies -3. Run `yarn build` to build all packages -4. Launch FluentUI Tester app for component testing (see `/apps/fluent-tester/README.md`) - -### Component Development - -**Component Location**: Stable JavaScript components are in `/packages/components/`, components under development are in -`/packages/experimental/`, and compatibility packages are in `/packages/shim/`. Modern native components and modules share -`/packages/agentic/native-core/`; follow its [organization plan](packages/agentic/native-core/README.md). - -**Component Structure**: Each component typically has: - -- `package.json` - Package definition with workspace dependencies -- `src/index.ts` - Main export file -- `src/.tsx` - Component implementation (requires `/** @jsxImportSource @fluentui-react-native/framework-base */` pragma) -- `src/.types.ts` - TypeScript type definitions -- `src/.styling.ts` - Styling and token definitions -- `src/..ts` - Platform-specific implementations -- `SPEC.md` - Component specification and usage documentation -- `MIGRATION.md` - Migration guide (for V1 components) -- `tsconfig.json`, `babel.config.js`, `jest.config.js`, `eslint.config.js` - -**Using Composition Framework**: Use `@fluentui-react-native/composition` for new components. For simpler components without slots/tokens, use the `stagedComponent` pattern from `@fluentui-react-native/framework-base`. - -**JSX Runtime**: All components use the modern automatic JSX runtime: - -- Add `/** @jsxImportSource @fluentui-react-native/framework-base */` at the top of `.tsx` files -- The custom jsx-runtime intercepts JSX calls to optimize slot rendering -- No need to import `withSlots` - it's handled automatically by the runtime -- Components using React Fragments (`<>...`) work automatically (Fragment is re-exported from the jsx-runtime) -- Packages using the jsx-runtime need `@fluentui-react-native/framework-base` in `devDependencies` - -**TypeScript Patterns**: - -- Slot functions automatically return `React.ReactElement`, so you can access `.props` directly without type assertions -- When checking for win32 platform: `Platform.OS === ('win32' as any)` - TypeScript doesn't recognize 'win32' but react-native-windows supports it -- Final render functions should return `FinalRender` with children as rest parameters: `(props: TProps, ...children: React.ReactNode[])` - -**Module Exports**: - -- **Do not use barrel exports (`export * from '...'`)** - wildcard re-exports break tree-shaking because bundlers cannot statically determine which symbols are used, so unused code is retained in consumers' bundles. Always use explicit named re-exports instead, e.g. `export { Foo, Bar } from './module'` and `export type { Baz } from './module'`. - -**Native Modules**: Callout and FocusZone implementations live in `@fluentui-react-native/native-core`; their former packages -are JS-only compatibility shims under `/packages/shim/`. Their JavaScript wrappers live in `src/legacy` and are exported -only from `@fluentui-react-native/native-core/legacy`, not native-core's root entrypoint. Components with native code: - -- Typically have one root slot wrapping the native component -- Use `codegenNativeComponent` for new architecture compatibility -- May use `constantsToExport` for default values from native side -- iOS/macOS: Include `.podspec` files -- Native applications must directly depend on native-core for native project discovery and autolinking - -For new modern native components and modules, use `@fluentui-react-native/native-core` rather than creating another standalone -package. Its Windows components are Fabric-only; macOS components support Paper and Fabric initially. Export shared APIs -from the root and scope platform APIs to `macos`, `windows` (React Native Windows), `win` (Windows/Win32), or `win32`. -Callout and FocusZone retain their existing public APIs and native registration names after migration. - -### Creating a New Component - -1. Create the package under `/packages/components/` or `/packages/experimental/` while it is - under development. Add modern native components/modules to the existing `/packages/agentic/native-core/` package instead. -2. Copy structure from existing component (e.g., Shimmer, Button) -3. Update `package.json` with correct name and dependencies (use `workspace:*` for internal packages) -4. Add the new package's `tsconfig.json` to the root `tsconfig.json` `references` so it joins the unified build -5. Create source files in `src/` -6. Add test page to FluentTester at `/apps/fluent-tester/src/TestComponents//` -7. Register test page in `testPages.tsx` and platform-specific `testPages..tsx` -8. Add E2E tests (see E2E Testing section) -9. Run `yarn` and `yarn build` from root -10. For Apple platforms: run `pod install` in test app directories - -### Theming - -Platform-specific themes are in `/packages/theming/`: - -- `android-theme/` - Android theming -- `apple-theme/` - iOS and macOS theming -- `win32-theme/` - Win32 theming -- `default-theme/` - Cross-platform defaults -- `theme-tokens/` - Token definitions -- `theme-types/` - TypeScript types for themes - -Components require `ThemeProvider` from `@fluentui-react-native/design/theming` to work properly. - -### Testing - -**Manual Testing**: Use FluentUI Tester app (`/apps/fluent-tester/`) for interactive component testing. Test pages are in `/apps/fluent-tester/src/TestComponents/`. - -**E2E Testing**: Required for all new components. Uses Appium + WebDriverIO. - -- E2E tests live in `/apps/E2E/src//` -- Each component needs: - - Page Object (`PageObject..ts`) - Interface to interact with test page - - Spec Document (`Spec..ts`) - Jasmine test cases - - Constants file in test component (`/apps/fluent-tester/src/TestComponents//consts.ts`) -- Test pages must include: - - `testID` on first section matching page object's `_pageName` - - Optional `e2eSections` prop for dedicated E2E test elements -- Run E2E tests: `yarn e2etest:` from `/apps/E2E/` - -**Unit Tests**: Component-specific Jest tests where present, typically in `src/` directories. Run them with `yarn test` in a package or `yarn lage test` from the root. After a major rework, run `yarn lage test --no-cache` once at the root to ensure all tests re-run and pass without stale cache hits; this resets the Lage cache so a normal incremental `yarn lage test` works afterward. - -### Platform-Specific Development - -**iOS/macOS**: - -- May wrap native controls from FluentUI Apple -- Requires `.podspec` files for native modules -- Run `pod install` after adding dependencies - -**Android**: - -- Platform-specific styling and tokens -- Uses `accessibilityLabel` for E2E selectors (other platforms use `testID`) - -**Win32**: - -- Separate test app at `/apps/win32/` -- Uses WinAppDriver for E2E testing - -**Windows (UWP)**: - -- Separate test app configuration -- Legacy support - -## Version Management - -**Changesets**: Used for change logs and versioning. - -- Run `yarn change` to create a change file when modifying packages -- Change files are required before merging PRs (validated in CI via `yarn change:check`) -- Changesets config in `.changeset/config.json` -- Major versions are disallowed (validated in CI via `.github/scripts/validate-changesets.mts`) -- Version bump PRs are created automatically by GitHub Actions; `yarn changeset:version` applies version bumps -- Publishing happens in Azure Pipelines using `.ado/azure-pipelines.publish.yml` - -## Important Notes - -- This is an **alpha-stage** library under active development -- **Requires TypeScript 7.0.2** (`typescript`, compiled via `tsc`); the unified build runs `tsc -b` over the root project-references graph -- **Uses Yarn 4 in pnpm mode** for dependency management (configured in `.yarnrc.yml`) -- **Uses modern automatic JSX runtime** - all components should use `@jsxImportSource @fluentui-react-native/framework-base` -- **Dynamic package extensions**: Common dev dependencies (TypeScript, Jest, ESLint, Prettier) are automatically added via `scripts/dynamic.extensions.mts` -- New packages must be added to the root `tsconfig.json` `references` to join the unified build -- Follow existing component patterns for consistency -- Test components using FluentUI Tester app before submitting PRs -- Platform differences should be documented in component `SPEC.md` files -- Use the newer composition framework (`@fluentui-react-native/composition`) for new components, not the deprecated foundation frameworks -- When importing V1 components, consider aliasing: `import { ButtonV1 as Button }` -- Slot functions return `React.ReactElement` - you can safely access `.props` without type assertions -- Avoid barrel exports (`export * from '...'`); use explicit named re-exports to preserve tree-shaking +Guidance for coding agents working in the FluentUI React Native monorepo, which implements Microsoft's Fluent Design +System for iOS, Android, macOS, Windows, and Win32. This file is always loaded. It holds repository safeguards and +routes each task to the workflow and references it needs; load only what the task requires. + +## How guidance is organized + +- **`AGENTS.md` files** hold safeguards and local invariants. Copilot CLI loads this file plus the `AGENTS.md` in the + working directory and each ancestor. From the repository root, read the scoped files below when a task touches them. +- **Skills** in [`.github/skills`](.github/skills) define task workflows. +- **[Shared references](references/agents/README.md)** in `references/agents/` hold reusable recipes and repository + knowledge, indexed by task. +- **Product contracts** (`SPEC.md`, `spec/`, `CONTRACT.md`), protocols, and package documentation stay with their + packages and govern component behavior. + +## Safeguards + +These apply to every task. + +- **Permissions:** [the repository hook](.github/hooks/agent-permissions.json) approves specific operations; it is not a + sandbox, and other operations use normal permission handling (R-PERM-001). Do not amend commits, discard other + people's changes, or rewrite history without explicit authorization (R-PERM-002). Never print authentication tokens + or commit personal permission files, account names, or credentials (R-PERM-003). Unrestricted screen capture, + `osascript`, debugger attachment, crash-report access, certificate-trust changes, and TCC resets need separate + authorization (R-PERM-007). Read the [permissions reference](references/agents/repository/permissions.md) before + using `gh api`, Pod, process, loopback `curl`, or capture operations, or changing the policy. +- **Validation:** run the owning workspace's declared scripts (R-VALID-001). Report each check as passed, failed, + skipped, not run, or unsupported; JavaScript tests and bundles are not native qualification (R-VALID-002). Finish + code or configuration changes with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). +- **Code:** no barrel exports (R-REPO-001); keep React Native fork imports in platform files (R-REPO-002); add a + changeset for published packages (R-REPO-003); never copy private source bodies into this public repository + (R-REPO-004). +- **Component generations:** v0 and v1 are maintenance-only; new components use the agentic workflow (R-V0-001, + R-V1-001). Classify code by imports and structure, not by path (R-CLASSIFY-001). +- **Native boundary:** before changing Swift, Objective-C, C++, headers, podspecs, Windows projects, codegen specs, + native registration, or a native-backed platform wrapper, use the + [native component skill](.github/skills/implement-native-component/SKILL.md), directly or as an overlay. iOS and + macOS changes must keep both CocoaPods-module and Office bridging-header builds working; follow the + [Apple and Office rules](references/agents/native/apple-office.md). + +## Route the task + +Classify the change with the [classification guide](references/agents/components/classification.md), then use: + +| Task | Skill | +| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| New component, or change or plan a change to an agentic component or primitive | [agentic-component-authoring](.github/skills/agentic-component-authoring/SKILL.md) | +| Agentic contract-only or contract-feasibility work | [agentic-component-contract-authoring](.github/skills/agentic-component-contract-authoring/SKILL.md) | +| Fix or maintain an existing v0 or v1 component (not native-core `/legacy`) | [maintain-v0-v1-component](.github/skills/maintain-v0-v1-component/SKILL.md) | +| Review a component change of any generation | [review-component](.github/skills/review-component/SKILL.md) | +| Native component, TurboModule, service, codegen, or a native-core `/legacy` wrapper | [implement-native-component](.github/skills/implement-native-component/SKILL.md) | +| Cross-component agentic extraction or dependency direction | [agentic-component-optimization](.github/skills/agentic-component-optimization/SKILL.md) | +| Storybook app, Metro, CocoaPods, or desktop native toolchain | [agentic-storybook-development](.github/skills/agentic-storybook-development/SKILL.md) | +| Dependency vulnerability remediation | [fix-security-issues](.github/skills/fix-security-issues/SKILL.md) | + +Other repository work starts from the [reference index](references/agents/README.md): architecture, commands and +validation, TypeScript, release, design tokens, focus, and native guidance. + +## Scoped instructions + +| Path | Scope | +| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | +| [packages/agentic/components](packages/agentic/components/AGENTS.md) | Agentic package invariants, contracts, and story tests | +| [packages/agentic/components/src](packages/agentic/components/src/AGENTS.md) | Source boundaries and extraction | +| [.../src/components](packages/agentic/components/src/components/AGENTS.md) | Higher-order components and the focus visual policy | +| [.../src/primitives](packages/agentic/components/src/primitives/AGENTS.md) | Public primitives | +| [packages/agentic/desktop-driver](packages/agentic/desktop-driver/AGENTS.md) | Desktop WebDriver driver | +| [packages/framework-base](packages/framework-base/AGENTS.md) | Shared component patterns | +| [packages/framework-base/src/accessibility](packages/framework-base/src/accessibility/AGENTS.md) | Accessibility helpers and native action qualification | +| [packages/framework/framework](packages/framework/framework/AGENTS.md) | v1 framework package | +| [packages/deprecated/foundation-compose](packages/deprecated/foundation-compose/AGENTS.md) | v0 framework package | +| [apps/storybook](apps/storybook/AGENTS.md) | Storybook app, native workflows, and diagnostics | + +## Quick facts + +- Yarn 4 in pnpm mode; TypeScript 7.0.2 compiled as one `tsc -b` project-references graph; Lage task orchestration. + New packages must be added to the root `tsconfig.json` `references`. +- `yarn build` builds the repository; `yarn lage test` and `yarn lage lint` test and lint packages; `yarn format` formats. + `yarn lage buildci` runs the CI aggregate, including `yarn check-agent-instructions`. `yarn clean-lage-cache` clears + the Lage cache before final validation. +- Details: [commands and validation](references/agents/repository/development.md), + [architecture](references/agents/repository/architecture.md), [TypeScript](references/agents/repository/typescript.md), + and [release](references/agents/repository/release.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 260b16e4888..ac4c660f1d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,11 +13,18 @@ This guide assumes you: There are some specific quirks to this repository that one should be familiar with. -### Old framework vs new framework (Compose V2) +### Component generations -There's [documentation](./packages/components/README.md) for how to author a control, specifically with the two frameworks [foundation-compose](./packages/deprecated/foundation-compose/README.md) and [foundation-composable](./packages/deprecated/foundation-composable/README.md). Since that documentation was written, we have written a new component framework (PR's [here](https://github.com/microsoft/fluentui-react-native/pull/335) and [here](https://github.com/microsoft/fluentui-react-native/pull/400)) at `packages/experimental/framework` that is simpler/easier to use, and is the new preferred way to create components. Components that use this new framework are located at `packages/experimental` and are copies of the controls located at `packages/components` which still uses the old frameworks. Any new components should use the new framework. The old documentation is still good to read, specifically for using foundation-compose, as a lot of it applies to the new component. +The repository contains three generations of components. Identify the generation from a component's imports and +structure before changing it; the [classification guide](./references/agents/components/classification.md) describes +how. -There are two directories where components exist, the normal `packages/components` and `packages/experimental`. In general, the `experimental` directory is where components and frameworks we are iterating on get placed. +- **v0:** components built on [foundation-compose](./packages/deprecated/foundation-compose/README.md). These are + maintenance-only. +- **v1:** components built on `@fluentui-react-native/framework` `compose` or `compressible`, usually exported with a + `V1` suffix from `packages/components` and `packages/experimental`. These are maintenance-only. +- **Agentic:** components in [`packages/agentic/components`](./packages/agentic/components), built from reviewed + contracts with framework-base patterns and design tokens. **All new components use this package.** ## Understanding the framework @@ -42,43 +49,65 @@ Tokens help us achieve simpler customization for complex higher order components ## Creating a new component -This section covers creating and adding a new component package to FluentUI React Native's monorepo. If you are instead working on an existing component and adding a native module, skip to the next two sections. +New components go in [`packages/agentic/components`](./packages/agentic/components): a styled higher-order component in +`src/components/`, or an unstyled building block in `src/primitives/`. Do not create new v0 or v1 component +packages under `packages/components` or `packages/experimental`; those generations accept maintenance only. Read the +package's [contributor instructions](./packages/agentic/components/AGENTS.md) and use +[Button](./packages/agentic/components/src/components/button) and +[Icon](./packages/agentic/components/src/primitives/icon) as the canonical examples. -Most components should use the compose framework as it offers the comprehensive set of patterns like tokens and slots, but if you're creating a simple component that doesn't require those patterns, there's a lighter pattern called [stagedComponent](./packages/framework-base/src/component-patterns/phased.ts). The stagedComponent pattern splits up the render function into two stages. Stage 1 handles building props and hook calls (best to separate the hook calls from the render tree since they rely on call order). Stage 2 returns the actual element tree, any conditional branching should happen here (Icon is a good example of using stagedCompoenent). +### Define the contract -1. Create a new directory in of these two locations, depending on your component: - - `fluentui-react-native/packages/components/` - - `fluentui-react-native/packages/experimental/` +1. A higher-order component starts with a reviewed contract: `SPEC.md` plus `spec/source.json`, `spec/tokens.yaml`, + `spec/accessibility.md`, `spec/interaction.md`, and `spec/usage.md`. Follow + [SPEC-SOURCE.md](./packages/agentic/components/SPEC-SOURCE.md) for provenance, review state, and source adapters. +1. A primitive instead keeps a colocated `CONTRACT.md` that describes its public API and renderer branches. +1. Run `yarn workspace @fluentui-react-native/components check:spec-contracts` to validate the contract. -1. Copy the files from the top level of another component (A good example of this is the Shimmer). Make sure to empty the changelog file, and modify the name of the component where appropriate. -1. Update your `package.json` file. This defines the name, fields, and dependencies of the npm package that will be published for this component. -1. As a pattern, we prefix `experimental` components with the word "experimental". For example, we have `@fluentui-react-native/experimental-button`. +### Add the source code -**Additionally, we require integrating your new component/test page with our E2E testing infrastructure. Whenever a new component/test page is created, it should map to new tests in /apps/fluent-tester/src/E2E. This is to create a sophisticated, in-depth testing infrastructure that ensures that we don't regress our component library in the future; giving our partners and customers the best experience possible. Please see [Authoring an E2E Test](https://github.com/microsoft/fluentui-react-native/tree/main/apps/E2E#authoring-e2e-test).** +A higher-order component uses these files, in dependency order: -Reach out to Samuel Freiberg with any questions related to E2E testing. +1. `.types.ts`: public props, slots, and resolved state. +1. `use.ts`: defaults, derived state, interaction, accessibility, and slot construction. +1. `.styles.ts` and `useStyles.ts`: token-based styles and slot style application. +1. `render.tsx`: pure slot rendering. Put `/** @jsxImportSource @fluentui-react-native/framework-base */` on + the first line of files that render slots; see `packages/framework-base/src/component-patterns/README.md`. +1. `.ts`: assembly and `displayName`. -### Adding the JS source code to your component +Expose the native root ref through the React 19 `ref` prop rather than `forwardRef`. Export the component and its public +types explicitly from `src/index.ts`, or from `src/primitives/index.ts` for a primitive; do not use `export *`. -1. Create a `src/` subdirectory in your component directory with a minimum of two files (listed below). You may optionally choose to subdivide your code however you wish; there are plenty of examples in the other components of FluentUI React Native. - 1. `index.ts` - - This is the file listed as `main` inside your package.json and simply exports other files. - 1. `.tsx` - This is the file that will actually define your function component, and compose it into a higher order component with slots, theming, and design tokens. - Note that we need the comment `/** @jsxImportSource @fluentui-react-native/framework-base */` as the first line of this file. This opts the file into the custom JSX runtime that handles slot rendering; an explanation can be found in `packages/framework-base/src/component-patterns/README.md`. Packages using this pragma must include `@fluentui-react-native/framework-base` in their `devDependencies`. - 1. `..tsx` (Optional) - - Optional extra files to subdivide your code however you see fit. You can also add platform specific files as you see fit. +### Test your component -### Adding a new test for your component to the test app +1. Colocate runtime tests, type tests, and snapshots with the implementation. +1. Add Storybook stories next to the component. Desktop behavior tests are `wdio` functions in the stories and run on + macOS, Windows, and Win32 through the [agentic Storybook app](./apps/storybook/README.md). +1. Run the package's declared scripts, such as `yarn workspace @fluentui-react-native/components test`, plus `build`, + `lint`, and `format`. +1. Before opening a pull request, run `yarn clean-lage-cache` and then `yarn lage buildci` from the repository root. -1. In FluentTester (our test app), we'll need to add an test component. The test components are located under `fluentui-react-native/apps/fluent-tester/src/TestComponents`. -1. Make a subdirectory for your new component at this path, as well as adding an entry for your test in `testPages.tsx` and `testPages..tsx` -1. Create your tests in this new subdirectory. Pattern matching off an existing control's tests will greatly help. -1. If your component contains a native module, you will also need to add the path to your component's `podspec` (that we created earlier) in your test app's Podfile. This extra step is due to the fact that FluentTester has separate platform test apps that each share a common JS package. The react native community CLI does not support [autolinking transitive dependencies](https://github.com/react-native-community/cli/issues/1347), so we need to add it manually here. -1. Add your new module as a dependency in `apps/fluent-tester/package.json` and run `yarn && yarn build` from the root folder. - 1. If your component has native apple code, run `pod install` from the corresponding macOS or iOS test app folders (`apps/fluent-tester/macos` or `apps/fluent-tester/ios` respectively). -1. Run the test app and you should see your new test! +### Maintaining v0 and v1 components + +Fixes to existing v0 and v1 components keep their current framework, public API, and tests: + +1. FluentTester test pages are located under `apps/tester-core/src/TestComponents`. Register a page in + `apps/tester-core/src/testPages.ts`, whose `platforms` array lists the platforms where it appears. +1. End-to-end tests live in `apps/E2E/src/`. See + [Authoring an E2E Test](https://github.com/microsoft/fluentui-react-native/tree/main/apps/E2E#authoring-e2e-test), + and reach out to Samuel Freiberg with questions about E2E testing. +1. If a component contains a native module, add its `podspec` path to the FluentTester Podfile. The React Native + community CLI does not support + [autolinking transitive dependencies](https://github.com/react-native-community/cli/issues/1347). Then run + `yarn && yarn build` from the root, and run `pod install` from `apps/fluent-tester/macos` or `apps/fluent-tester/ios` + for native Apple code. ## Adding native code to your new component +New modern native components and TurboModules belong in +[`@fluentui-react-native/native-core`](./packages/agentic/native-core/README.md) rather than a new standalone package. +The sections below describe the native wrappers used by existing v0 and v1 packages. + Through the power of [Native Modules](https://reactnative.dev/docs/turbo-native-modules-introduction), we are able to create components that are comprised of native platform code, rather than JS. This is particularly useful if you want platform specific behavior, or if you want a component that feels much more aligned to it's specific platform. The downside is you must implement the Native module for every platform you wish to support. It's worth investigating whether you truly need a native module, or if a more cross platform JS implementation is the better approach. There are a few caveats to know of adding a native module to a FluentUI React Native component: diff --git a/apps/E2E/README.md b/apps/E2E/README.md index 7302135c77b..fde98703f86 100644 --- a/apps/E2E/README.md +++ b/apps/E2E/README.md @@ -143,7 +143,7 @@ Please follow this structure for the new component. ## Create New Constants The way we our automation framework interacts with our test app is by selecting UI components based on a string value. For this, we have a **consts.ts** file for each component that defines these values. -You'll want to make one for your component under **apps/fluent-tester/src/TestComponents/_your-component_/consts.ts**. +You'll want to make one for your component under **apps/E2E/src/_your-component_/consts.ts** and export it from `apps/E2E/src/index.consts.ts`; test pages import the constants from `@fluentui-react-native/e2e-testing`. **You can simply copy/paste a _consts.ts_ file from another component, and just change the name of the component in the const names and the values.** @@ -254,7 +254,7 @@ export const CheckboxTest = () => { The spec document is where the tests will be written. We use [Jasmine](https://jasmine.github.io/), an open-sourced testing framework for JavaScript. The spec document imports a page object and uses it to manipulate the UI and uses 'expect' and other Jasmine statements to ensure proper functionality. -Spec documents should be put in apps/fluent-tester/src/E2E/\_ _ComponentToBeTested_ /specs/. +Spec documents should be put in apps/E2E/src/_ComponentToBeTested_/specs/. ``` describe('Click on each test page and check if it renders', function() { diff --git a/apps/storybook/AGENTS.md b/apps/storybook/AGENTS.md index 37f5a638fae..9367f308453 100644 --- a/apps/storybook/AGENTS.md +++ b/apps/storybook/AGENTS.md @@ -33,6 +33,26 @@ Read [`agent-map.yaml`](agent-map.yaml) first for the compact architecture, look - `app.json` owns the custom `storybook.testIDPrefix`. Do not create another app identity file or duplicate the prefix in runtime source. +## Native diagnostics + +The repository Yarn approval covers these workflows; see the +[agent permissions reference](../../references/agents/repository/permissions.md#native-diagnostics-and-capture) for +capture limits. From this directory: + +| Need | Preferred command | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Project generation and Pod installation | `yarn storybook prep --macos` | +| Bundle failures | `yarn storybook --verbose bundle --` | +| Native build failures | `yarn storybook --verbose build --` (not Win32) | +| Driver identity, signing, and macOS privacy diagnostics | `yarn desktop-driver doctor --platform macos --permissions` | +| Actual listener ports and target identity | `yarn storybook instance --` | +| Target-scoped screenshots | `yarn desktop-driver agent screenshot --platform --url --target --artifacts artifacts/ --name ` | +| Runtime trees and crash reproduction | `yarn desktop-driver agent describe ...` and `yarn storybook --verbose smoke --` | + +For `agent` commands, use `--platform macos` on macOS and `--platform windows` for both Windows and Win32. Inspect +retained command logs under `artifacts/storybook-commands` and platform artifacts rather than redirecting output to +arbitrary locations. + ## Desktop Driver workflow - Use `yarn storybook manifest --` to validate static story-plan diff --git a/lage.config.mjs b/lage.config.mjs index e8270be5c32..e2fc9bff641 100644 --- a/lage.config.mjs +++ b/lage.config.mjs @@ -37,6 +37,9 @@ const config = { // tsc project level build has its own caching cache: false, }, + 'check-agent-instructions': { + cache: false, + }, 'check-publishing': { cache: false, }, @@ -65,8 +68,8 @@ const config = { }, // ── Pipeline aliases ─────────────────────────────────────────────────── - 'repo-checks': ['lint-repo', 'check-publishing'], - buildci: ['lint-repo', 'check-publishing', '@fluentui-react-native/root#root-build', 'test', 'lint'], + 'repo-checks': ['lint-repo', 'check-publishing', 'check-agent-instructions'], + buildci: ['lint-repo', 'check-publishing', 'check-agent-instructions', '@fluentui-react-native/root#root-build', 'test', 'lint'], // ── Worker tasks ─────────────────────────────────────────────────────── 'test-links': { diff --git a/package.json b/package.json index c78c5ba5b4e..d73f29e8c15 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "bundle:repo": "lage bundle", "clean": "tsc -b --clean && lage clean", "clean-all": "node ./scripts/src/preinstall/clean-all.ts", + "clean-lage-cache": "lage cache --clear", "change": "node .github/scripts/change.mts", "changeset:version": "node .github/scripts/changeset-version-with-postbump.mts", "change:check": "node .github/scripts/change.mts --check", + "check-agent-instructions": "node ./scripts/src/cli.ts check-agent-instructions", "check-publishing": "node ./scripts/src/cli.ts check-publishing", "lint-fix": "cross-env FURN_FIX_MODE=true lage lint", "root-build": "tsc -b", diff --git a/packages/agentic/components/AGENTS.md b/packages/agentic/components/AGENTS.md index ff96a662f3f..70f325cf433 100644 --- a/packages/agentic/components/AGENTS.md +++ b/packages/agentic/components/AGENTS.md @@ -1,44 +1,40 @@ # Agentic Components authoring These instructions apply to `packages/agentic/components` and its descendants. Keep this file limited to durable package -invariants; detailed authoring recipes live in the -[agentic component authoring skill](../../../.github/skills/agentic-component-authoring/SKILL.md). +invariants; detailed recipes live in the shared [agentic references](../../../references/agents/README.md), routed by +the [agentic component authoring skill](../../../.github/skills/agentic-component-authoring/SKILL.md). ## Routing -- Higher-order components live in `src/components`; read `src/components/AGENTS.md`. -- Primitive components live in `src/primitives`; read `src/primitives/AGENTS.md`. -- Story files are library source and follow the tests and stories reference. -- Focus work starts with the [common focus instructions](../../../.github/skills/agentic-component-authoring/references/focus.md), - then the [Windows/Win32](../../../.github/skills/agentic-component-authoring/references/focus-windows.md) or - [macOS](../../../.github/skills/agentic-component-authoring/references/focus-macos.md) detail. V1 Win32 is the +- Implementation and new components use the + [agentic component authoring skill](../../../.github/skills/agentic-component-authoring/SKILL.md); reviews use + [review-component](../../../.github/skills/review-component/SKILL.md). +- Higher-order components live in `src/components`; read [its instructions](src/components/AGENTS.md). +- Primitive components live in `src/primitives`; read [its instructions](src/primitives/AGENTS.md). +- Story files are library source and follow [tests and stories](../../../references/agents/components/agentic/tests-and-stories.md). + Executable tests are top-level `wdio` callbacks or maps of named callbacks typed with `import type` of `WdioStory` + from `storybook-desktop/testing`. Keep callbacks self-contained and dynamically import Node helpers inside them. + Each named case gets a fresh story, session, and worker. Use stable `testID` selectors and the injected `platform` + for real platform differences, with explicit skips for unsupported capabilities. Shared focus tests run on macOS, + Windows, and Win32; gate only genuinely platform-specific assertions. Legacy `parameters.desktopDriver` plans remain + only in runner compatibility fixtures. +- Focus work starts with the [common focus instructions](../../../references/agents/focus/common.md), + then the [Windows/Win32](../../../references/agents/focus/windows-win32.md) or + [macOS](../../../references/agents/focus/macos.md) detail. V1 Win32 is the behavioral baseline for both Windows endpoints; renderer-specific plumbing remains separate. -- Author executable tests under top-level `wdio`, as one callback or a map - of named callbacks. All catalog tests use this pattern; legacy - `parameters.desktopDriver` plans remain only in runner compatibility fixtures. - Use stable `testID` selectors and the injected target `platform` for real - platform differences, with explicit skips for unsupported capabilities. -- Shared focus tests run on macOS, Windows, and Win32. Gate only genuine - platform-specific assertions, such as Windows pointer-focus, rather than the - entire case. Follow the tests-and-stories reference for native activation - timing, macOS Keyboard navigation prerequisites, and pass/skip evidence. -- Executable tests are typed - with `WdioStory` from `storybook-desktop/testing` using `import type`. - Keep callbacks self-contained; dynamically import Node helpers inside - them so the shared Babel transform can exclude them from native bundles. - Each named case gets a fresh story, session, and worker; do not share native - element handles or depend on previous cases. - Storybook application, native project, Metro, bundle, or CocoaPods work follows the [Storybook instructions](../../../apps/storybook/AGENTS.md) and the `agentic-storybook-development` skill. -- Native React Native Windows Fabric component work follows the - [Windows Fabric native component reference](../../../.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md). +- Native views, codegen, or registration follow the + [native component skill](../../../.github/skills/implement-native-component/SKILL.md); React Native Windows Fabric + views also follow the [Windows Fabric reference](../../../references/agents/native/windows-fabric.md). ## Package invariants -- Create or revise source-backed contracts through the - [agentic component contract authoring skill](../../../.github/skills/agentic-component-contract-authoring/SKILL.md). - It routes Flex/X3, Fluent v9, HTML/CSS, visual, local-foundation, and - composite evidence before implementation. +- **R-AGENTIC-001:** Before implementing a higher-order component whose contract is absent, stale, still a draft, or + accompanied by new source evidence, create or revise its source-backed contract through the + [agentic component contract authoring skill](../../../.github/skills/agentic-component-contract-authoring/SKILL.md) + and complete its review gate. Never mechanically promote a web-authored specification. The skill routes Flex/X3, + Fluent v9, HTML/CSS, visual, local-foundation, and composite evidence. - Use the repository's `flex-authoring` profile and `flex-components:` only for a pinned Flex/X3 source. It is not a prerequisite for other source adapters. @@ -53,10 +49,10 @@ invariants; detailed authoring recipes live in the - Use `src/components/button` as the canonical higher-order implementation and `src/primitives/icon` as the canonical primitive. - Keep public props and slots small, typed, and spec-driven. -- This package targets React 19.1.4 or newer. Expose a stable native root ref through the `ref` prop, receive it as part - of the component props, and forward it to the declared root slot. Do not use `forwardRef`. +- **R-AGENTIC-002:** This package targets React 19.1.4 or newer. Expose a stable native root ref through the `ref` prop, + receive it as part of the component props, and forward it to the declared root slot. Do not use `forwardRef`. - Export higher-order components and public types explicitly from `src/index.ts`; export primitives and their public types - explicitly from `src/primitives/index.ts`. Never use wildcard exports. + explicitly from `src/primitives/index.ts`. Never use wildcard exports (R-REPO-001). - Colocate runtime tests, type tests, and Storybook stories with the implementation. - Include every test-bearing story in `tsconfig.stories.json`. The representative runner contract rejects legacy catalog plans and missing story type coverage. @@ -65,5 +61,5 @@ invariants; detailed authoring recipes live in the - Use package scripts for format, lint, build, tests, and snapshots. - Do not copy web-only APIs, CSS behavior, or DOM assumptions into React Native. - Do not copy or mechanically transform private source bodies into this public - package. Record source identifiers and digests, then author the local React + package (R-REPO-004). Record source identifiers and digests, then author the local React Native contract. diff --git a/packages/agentic/components/src/AGENTS.md b/packages/agentic/components/src/AGENTS.md index 899182d7ceb..f324100ddcf 100644 --- a/packages/agentic/components/src/AGENTS.md +++ b/packages/agentic/components/src/AGENTS.md @@ -27,7 +27,7 @@ the change crosses component boundaries. - Align native text with View layout, not synthetic text heights or baseline offsets. A font's em size, line box, and visible glyph are different measurements. Use `Icon` for font icons and `LayoutStableText` for reserved labels; follow - the [native alignment rules](../../../../.github/skills/agentic-component-authoring/references/styles-and-tokens.md#native-text-and-vertical-alignment) + the [native alignment rules](../../../../references/agents/components/agentic/styles-and-tokens.md#native-text-and-vertical-alignment) for editors, mixed-size text rows, and regression coverage. - Audit dependency direction before introducing shared code. diff --git a/packages/agentic/components/src/components/AGENTS.md b/packages/agentic/components/src/components/AGENTS.md index d2511ab848a..9574249a77a 100644 --- a/packages/agentic/components/src/components/AGENTS.md +++ b/packages/agentic/components/src/components/AGENTS.md @@ -67,9 +67,9 @@ audit. ## Focus visual policy -Read [common focus authoring](../../../../../.github/skills/agentic-component-authoring/references/focus.md), -then [Windows/Win32](../../../../../.github/skills/agentic-component-authoring/references/focus-windows.md) or -[macOS](../../../../../.github/skills/agentic-component-authoring/references/focus-macos.md). +Read [common focus authoring](../../../../../references/agents/focus/common.md), +then [Windows/Win32](../../../../../references/agents/focus/windows-win32.md) or +[macOS](../../../../../references/agents/focus/macos.md). The common file owns ref, state, activation, scope, and styling invariants; platform files own detailed native adaptation. V1 Win32 models observable behavior for both Office Win32 and Windows Fabric. @@ -90,8 +90,8 @@ identities by following a proposal before its component contract is reviewed. Th ## Focused references - [Contract and source adaptation](../../../../../.github/skills/agentic-component-contract-authoring/SKILL.md) -- [Types and slots](../../../../../.github/skills/agentic-component-authoring/references/types-and-slots.md) -- [State and accessibility](../../../../../.github/skills/agentic-component-authoring/references/state-and-accessibility.md) -- [Styles and tokens](../../../../../.github/skills/agentic-component-authoring/references/styles-and-tokens.md) -- [Rendering and assembly](../../../../../.github/skills/agentic-component-authoring/references/rendering.md) -- [Tests and stories](../../../../../.github/skills/agentic-component-authoring/references/tests-and-stories.md) +- [Types and slots](../../../../../references/agents/components/agentic/types-and-slots.md) +- [State and accessibility](../../../../../references/agents/components/agentic/state-and-accessibility.md) +- [Styles and tokens](../../../../../references/agents/components/agentic/styles-and-tokens.md) +- [Rendering and assembly](../../../../../references/agents/components/agentic/rendering.md) +- [Tests and stories](../../../../../references/agents/components/agentic/tests-and-stories.md) diff --git a/packages/agentic/components/src/components/checkbox/spec/accessibility.md b/packages/agentic/components/src/components/checkbox/spec/accessibility.md index 4612415a61f..053859326ba 100644 --- a/packages/agentic/components/src/components/checkbox/spec/accessibility.md +++ b/packages/agentic/components/src/components/checkbox/spec/accessibility.md @@ -24,6 +24,8 @@ press. Custom and disabled actions are still forwarded. A standalone `activate` event is not an additional toggle path. The [shared action foundation](../../../../../../framework-base/src/accessibility/AGENTS.md) +defines the action contract, and its +[transport evidence](../../../../../../../references/agents/accessibility/action-transport-evidence.md) records RNW 0.81.35, Win32 0.81.8, and RNmacOS 0.81.9 source evidence. macOS Fabric custom actions emit the declared name verbatim and ignore the display label; this does not establish default AXPress support. Native diff --git a/packages/agentic/components/src/components/switch/spec/accessibility.md b/packages/agentic/components/src/components/switch/spec/accessibility.md index aea120f61d2..ecf8cce7e17 100644 --- a/packages/agentic/components/src/components/switch/spec/accessibility.md +++ b/packages/agentic/components/src/components/switch/spec/accessibility.md @@ -32,6 +32,8 @@ caller handler, as do unrelated custom actions. No action synthesizes `onPress`, and `activate` is not an additional toggle path. The [shared action foundation](../../../../../../framework-base/src/accessibility/AGENTS.md) +defines the action contract, and its +[transport evidence](../../../../../../../references/agents/accessibility/action-transport-evidence.md) records RNW 0.81.35, Win32 0.81.8, and RNmacOS 0.81.9 source evidence. macOS Fabric invokes custom actions by their declared names and ignores display labels; default AXPress support cannot be inferred from that path. diff --git a/packages/agentic/components/src/components/tab/spec/accessibility.md b/packages/agentic/components/src/components/tab/spec/accessibility.md index 73dbfc36620..80baa8ccff6 100644 --- a/packages/agentic/components/src/components/tab/spec/accessibility.md +++ b/packages/agentic/components/src/components/tab/spec/accessibility.md @@ -33,6 +33,8 @@ selecting. No accessibility action synthesizes `onPress`, and `activate` is not an additional selection path. The [shared action foundation](../../../../../../framework-base/src/accessibility/AGENTS.md) +defines the action contract, and its +[transport evidence](../../../../../../../references/agents/accessibility/action-transport-evidence.md) records RNW 0.81.35, Win32 0.81.8, and RNmacOS 0.81.9 source evidence. macOS Fabric custom actions emit the exact declared names and ignore display labels; default AXPress support is not established by that path. Native diff --git a/packages/agentic/components/src/primitives/AGENTS.md b/packages/agentic/components/src/primitives/AGENTS.md index 1ec174b32e4..83da84136d9 100644 --- a/packages/agentic/components/src/primitives/AGENTS.md +++ b/packages/agentic/components/src/primitives/AGENTS.md @@ -8,15 +8,16 @@ the canonical implementation. - Primitives are public only from `@fluentui-react-native/components/primitives`; do not re-export them from the package root. - Give primitive components stable public names. Suffix public composition helpers whose contracts may evolve with `_unstable`, matching higher-order component pipeline helpers. -- Maintain a colocated `CONTRACT.md` for every public primitive. Derive its test obligations from that contract, its public - types, and each renderer branch rather than creating an upstream-backed `SPEC.md`. +- **R-PRIMITIVE-001:** Maintain a colocated `CONTRACT.md` for every public primitive and keep primitives unstyled. Derive + test obligations from that contract, its public types, and each renderer branch rather than creating an + upstream-backed `SPEC.md`. - Primitives are unstyled building blocks. Do not read themes, apply design tokens, or choose product appearance defaults. - Define the smallest acceptance contract needed for `SlotProp` consumption. - Prefer a hook-free `directComponent` when the primitive only selects an inner component or transforms props. -- For a primitive with one stable native root type, include `PropsWithRefOf` in its public props and pass - the React 19.1.4 `ref` prop to that root without `forwardRef`. A primitive whose renderer does not have one stable - imperative instance type must omit `ref` and document that exception in its `CONTRACT.md`. +- **R-PRIMITIVE-002:** For a primitive with one stable native root type, include `PropsWithRefOf` in its + public props and pass the React 19.1.4 `ref` prop to that root without `forwardRef`. A primitive whose renderer does + not have one stable imperative instance type must omit `ref` and document that exception in its `CONTRACT.md`. - Forward accessibility and test props without replacing consumer values. - Keep alternative source or renderer props mutually exclusive. - Add compile-time `SlotProp` coverage and runtime coverage for every renderer branch. @@ -28,6 +29,6 @@ the canonical implementation. ## Focused references -- [Types and slots](../../../../../.github/skills/agentic-component-authoring/references/types-and-slots.md) -- [Rendering and assembly](../../../../../.github/skills/agentic-component-authoring/references/rendering.md) -- [Tests and stories](../../../../../.github/skills/agentic-component-authoring/references/tests-and-stories.md) +- [Types and slots](../../../../../references/agents/components/agentic/types-and-slots.md) +- [Rendering and assembly](../../../../../references/agents/components/agentic/rendering.md) +- [Tests and stories](../../../../../references/agents/components/agentic/tests-and-stories.md) diff --git a/packages/agentic/native-core/README.md b/packages/agentic/native-core/README.md index bd101489108..a5d192ba918 100644 --- a/packages/agentic/native-core/README.md +++ b/packages/agentic/native-core/README.md @@ -167,7 +167,7 @@ hosts become Fabric-only, without changing the public JS API. The Windows component project must require the New Architecture and fail clearly for a Paper build. Use the repository's -[Windows Fabric native component reference](../../../.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md) +[Windows Fabric native component reference](../../../references/agents/native/windows-fabric.md) and verify APIs against the installed RNW version. Win32 integration is separate from React Native Windows Paper support; adding a Win32 export does not authorize a second RNW view-manager implementation. diff --git a/packages/deprecated/foundation-compose/AGENTS.md b/packages/deprecated/foundation-compose/AGENTS.md index 218573b309d..92df74092c2 100644 --- a/packages/deprecated/foundation-compose/AGENTS.md +++ b/packages/deprecated/foundation-compose/AGENTS.md @@ -1,195 +1,25 @@ -# Legacy v0 component authoring +# Legacy v0 component framework -These instructions apply to `packages/deprecated/foundation-compose` and to work -on components that use `@uifabricshared/foundation-compose`. They describe the -legacy/v0 framework only. Do not use this pattern for a new component unless the -task explicitly requires v0 compatibility. +These instructions apply to `packages/deprecated/foundation-compose`, which implements the repository's legacy v0 +`compose` framework. The consumer recipe, recognition guide, and review checklist for v0 components live in the +shared [v0 reference](../../../references/agents/components/v0.md); read it before changing this package or any v0 +component. v0 is maintenance-only (R-V0-001). -## Recognizing v0 code +## Package boundaries -Treat code as v0 when several of these are present: - -- imports from `@uifabricshared/foundation-compose`, - `foundation-composable`, `foundation-settings`, or `foundation-tokens`; -- a descriptor such as `IButtonType` with `props`, `tokens`, `slotProps`, - optional `state`, and optional `statics`; -- `compose({...})`, `IUseComposeStyling`, - `ISlots<...>`, and `mergeSettings(...)`; -- settings shaped as `{ tokens, root, ...slots, _overrides, _precedence }`; -- an implementation under `deprecated/` or `legacy/`, or an old un-suffixed - export beside a newer `V1` export. - -Representative implementations are -`packages/components/Button/src/deprecated/Button.tsx`, -`packages/components/Checkbox/src/deprecated/Checkbox.tsx`, -`packages/components/Link/src/legacy/Link.tsx`, -`packages/components/Persona/src/Persona.tsx`, and -`packages/components/Stack/src/Stack.tsx`. - -## Framework model and public API - -`compose` combines the lower-level composable, themed-settings, settings, and -token systems. Its phases are: - -1. resolve and cache layered `settings`; -2. resolve token props into cached slot props/styles through `styles`; -3. run `usePrepareProps` for hooks, state, refs, and instance-specific props; -4. call the hook-free `render` function with prepared `Slots`. - -The package publicly exports `compose`, `initializeStyling`, and the compose -option/return/settings/styling and type-extraction types listed in `src/index.ts`. -The returned component exposes `__composable`, `.compose(options)`, and -`.customize(...settings)`. Prefer the strongly typed component methods: - -- `.compose(...)` replaces or augments behavior, slots, styles, settings, or - render without adding a React wrapper. Settings are appended to the base - settings. See - `packages/components/Button/src/deprecated/PrimaryButton/PrimaryButton.ts`. -- `.customize(...)` only appends old-style settings entries. Its input retains - `tokens`, `_overrides`, `_precedence`, and slot-prop keys; it is not the newer - flat token customization format. -- `statics` attaches subcomponents, as in `Stack.Item`. - -The exported two-argument `compose(options, base)` form is the underlying API; -use `Base.compose(options)` when the base is already a typed composed component. -`initializeStyling` is framework infrastructure that `compose` installs -automatically, not a routine component authors normally call. - -## Component shape - -Define a shared descriptor type and keep slot names aligned: - -```ts -interface IExampleType { - props: IExampleProps; - tokens: IExampleTokens; - slotProps: IExampleSlotProps; // must contain root - state: IExampleState; -} - -export const Example = compose({ - displayName: exampleName, - settings, - usePrepareProps, - render, - slots: { root: View, content: Text }, - styles: { root: [backgroundColorTokens], content: [textTokens] }, -}); -``` - -Typical files are `.types.ts`, `.settings.ts`, -`.tokens*.ts`, `.tsx`, `index.ts`, and colocated Jest -tests/snapshots. Preserve existing public I-prefixed types, deprecated exports, -display names, prop names, and v0/v1 aliases. - -For a one-slot component, `compose` supplies default preparation and rendering: -it applies styling and user props to `root`. See -`packages/components/Text/src/deprecated/Text.tsx` and -`packages/components/Stack/src/StackItem/StackItem.tsx`. Multiple slots require -an explicit `render`. Once custom `usePrepareProps` is supplied, it must put the -public/native props on the appropriate slot; the framework does not do that -automatically. - -## Slots and rendering - -- `slotProps` is a named props collection with a required `root`. -- Define each slot as a component/string or as `{ slotType, filter }`. Use - existing filters such as `filterViewProps`, `filterTextProps`, and - `filterImageProps` so component-only/token props do not reach native views. -- Merge prepared props with styling, normally - `mergeSettings(useStyling(userProps, lookup), { root: rootProps, ... })`. - Later entries win, including merged styles and class names. -- Render prepared slots as `` / ``. Their prepared - props are already captured; pass only additional render-time props in JSX. -- Children are removed before preparation and supplied to `render` as - `...children`. Decide explicitly where they belong, as Button, Link, and Stack - do. Conditional slot rendering is safe because all hooks already ran. -- Put `/** @jsxImportSource @fluentui-react-native/framework-base */` first in - files that render callable `Slots` with JSX. This pragma supports the legacy - slot runtime; it does not make the component a modern framework-base pattern. - -## Settings, tokens, and styling - -- Type settings as `IComposeSettings`. Entries are layered in - array order and may be a settings object, a theme settings name string, or a - `(theme) => settings` function. Keep objects/functions stable and never mutate - cached settings. -- Use `_precedence` to define state application order and `_overrides` for - state-specific settings; nested overrides are supported. Settings names such - as `buttonName` load theme-provided component settings. -- `useStyling(props)` uses props as the default override mask. For derived - interaction state, pass an override lookup function or mask, as in legacy - Button, Checkbox, and Link. -- Declare token-to-slot processing in `styles`. Reuse operation sets such as - `backgroundColorTokens`, `borderTokens`, `foregroundColorTokens`, and - `textTokens`; use `{ source, target, lookup }` for mappings/theme lookup. -- For component-specific calculations, use `styleFunction` and list every token - key that affects the result. See - `packages/components/Persona/src/Persona.tokens.root.ts`, - `packages/components/Persona/src/Persona.tokens.texts.ts`, and - `packages/components/Stack/src/Stack.tokens.ts`. Accurate key lists are - required for memoization. -- When a slot is itself composed and supports a token, token processing can - forward that token to the child instead of producing a parent-owned style. - Preserve compatible child token contracts when replacing composed slots. -- Put theme/default/state values in settings and token-derived style in - `styles`. Put highly variable values such as text, accessibility props, - handlers, sources, and refs into `usePrepareProps` slot props. - -## State, hooks, and refs - -- `usePrepareProps` and `useStyling` are hook phases: call them on every render - and never conditionally. Hooks, context, controlled/uncontrolled state, and - interaction hooks belong in `usePrepareProps`. -- Return `{ slotProps, state }`; `state` is simply the typed payload for - `render`, not necessarily React state. Keep `render` hook-free. -- Preserve the v0 `componentRef` API where present. Convert it to the native root - `ref` during preparation, commonly with `useViewCommandFocus`; type ref-bearing - roots with `React.PropsWithRef<...>`. For imperative native APIs, use an - internal native ref plus `useImperativeHandle`, as in - `packages/agentic/native-core/src/legacy/callout/Callout.tsx`. -- Do not introduce `forwardRef` or a new public `ref` convention into an - established v0 API without an explicit migration requirement. - -## Platform-specific implementations - -Use normal React Native resolution for `.android.tsx`, `.macos.tsx`, -`.windows.tsx`, and `.win32.tsx`, and for platform settings files. Keep the same -public descriptor/props across variants while allowing different native slots -and preparation logic. Examples include deprecated Button on Android and -RadioButton on macOS/Win32. Keep imports from platform React Native forks inside -their platform files, per the repository root guidance. - -## Legacy boundaries - -- Do not rewrite v0 code to `@fluentui-react-native/composition`, modern - `@fluentui-react-native/framework`, or framework-base - `phasedComponent`/`directComponent`/`useSlot` patterns as part of maintenance. -- Do not apply v1 slot types, flat state tokens, `useTokens`, or the new - `.customize({ token: value, state: {...} })` shape to v0 code. -- Do not “simplify” away `mergeSettings`, the settings name entry, override - lookup, prop filters, token key lists, or the split prepare/render phases; - each participates in compatibility, composition, or caching. -- Prefer surgical fixes. Migration to v1 is a separate task and must account for - documented prop, token, slot, and export differences such as - `packages/components/Button/MIGRATION.md`. +- `compose` combines `foundation-composable`, themed settings, `foundation-settings`, and `foundation-tokens`. + Changes to settings override or merge semantics, or to token processing, also require those packages' tests. +- The package publicly exports `compose`, `initializeStyling`, and the compose option, return, settings, styling, and + type-extraction types listed in `src/index.ts`. `initializeStyling` is infrastructure that `compose` installs + automatically. +- Preserve settings layering, filters, token key lists, and the prepare/render split that consumers rely on (R-V0-002). ## Validation -Run the smallest package-scoped checks covering the change: - ```bash yarn workspace @uifabricshared/foundation-compose build yarn workspace @uifabricshared/foundation-compose lint +yarn workspace @uifabricshared/foundation-compose test ``` -For behavior changes, also test/build the owning consumer package, for example: - -```bash -yarn workspace @fluentui-react-native/button test -yarn workspace @fluentui-react-native/button build -``` - -Update existing snapshots only after reviewing the rendered slot tree. Changes -to settings override/merge semantics or token processing also require the -`foundation-settings` or `foundation-tokens` unit tests respectively. +For behavior changes, also build and test affected consumer packages, such as `@fluentui-react-native/button`. diff --git a/packages/framework-base/AGENTS.md b/packages/framework-base/AGENTS.md index d565058a9b7..571d1784ce6 100644 --- a/packages/framework-base/AGENTS.md +++ b/packages/framework-base/AGENTS.md @@ -51,7 +51,7 @@ entrypoint's React-only import safety. ## Focus and native refs -Use the [common focus instructions](../../.github/skills/agentic-component-authoring/references/focus.md) +Use the [common focus instructions](../../references/agents/focus/common.md) and their platform breakouts when adding focus-target or interaction abstractions. Reuse slot ref composition and cleanup; preserve this package's older React compatibility while agentic components use React 19 ref props. Keep proposed diff --git a/packages/framework-base/src/accessibility/AGENTS.md b/packages/framework-base/src/accessibility/AGENTS.md index 914553ef9cc..c70cd15a2cf 100644 --- a/packages/framework-base/src/accessibility/AGENTS.md +++ b/packages/framework-base/src/accessibility/AGENTS.md @@ -27,27 +27,20 @@ agree, and duplicate declarations can produce duplicate native callbacks. The current transport contract is: -| Platform | Toggle | Select | Evidence and limits | -| --------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Windows Fabric | `toggle` | `select` | RNW 0.81.35 `CompositionDynamicAutomationProvider.cpp` dispatches lowercase names. `UiaHelpers.cpp::DispatchAccessibilityAction` compares case-sensitively and emits once for every matching declaration. | -| Office Win32 | `Toggle` | `Select` | `@office-iss/react-native-win32` 0.81.8 `ViewAccessibility.d.ts` declares these UIA action names; existing FURN V1 Checkbox and Switch use `Toggle`. Native UIA invocation still needs target-platform verification. | -| macOS | `Toggle` | `Select` | RNmacOS 0.81.9 Fabric `RCTViewComponentView.mm::accessibilityCustomActions` exposes the declared names verbatim; `didActivateAccessibilityCustomAction` emits that exact name. These are custom actions, not an inferred `AXPress` mapping. | -| Other platforms | `Toggle` | `Select` | Retains existing custom declarations; this is not native pattern or gesture qualification. Verify a renderer before claiming support. | +| Platform | Toggle | Select | Evidence | +| --------------- | -------- | -------- | -------------------------------------------------------------------------- | +| Windows Fabric | `toggle` | `select` | C-A11Y-001 | +| Office Win32 | `Toggle` | `Select` | C-A11Y-002; native UIA invocation still needs target-platform verification | +| macOS | `Toggle` | `Select` | C-A11Y-003; custom actions, not an inferred `AXPress` mapping | +| Other platforms | `Toggle` | `Select` | Existing custom declarations; not native pattern or gesture qualification | -These source paths are relative to the installed platform package: +The version-bound source evidence, including installed-package paths, is in the +[action transport evidence](../../../../references/agents/accessibility/action-transport-evidence.md). Verify a +renderer before claiming support on another platform. -- RNW: `Microsoft.ReactNative/Fabric/Composition/`. -- Win32: `Libraries/Components/View/ViewAccessibility.d.ts`. -- RNmacOS Fabric: `React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm`. -- RNmacOS Paper: `React/Views/RCTView.m`. - -RNmacOS Fabric constructs custom actions from `name`, ignoring `label`; keep -caller labels in JS rather than replacing them with guessed localized text. -Paper instead maps a custom action's label back to its declared name and -implements `accessibilityPerformPress` using `activate` or `onAccessibilityTap`. -Fabric's `accessibilityActivate` only forwards `onAccessibilityTap`, and is not -that AppKit press implementation. Do not map toggle/select to `activate`, -synthesize `onPress`, or add an AXPress fallback based on Paper evidence. +Keep caller labels in JavaScript rather than replacing them with guessed localized text, because macOS Fabric ignores +labels when constructing custom actions (C-A11Y-003). Do not map toggle/select to `activate`, synthesize `onPress`, or +add an AXPress fallback based on Paper behavior (C-A11Y-004). The resolver normalizes only the owned semantic action's two known spellings. It deduplicates exact resulting names, preserves caller order and the first diff --git a/packages/framework-base/src/component-patterns/README.md b/packages/framework-base/src/component-patterns/README.md index 127db2d0a07..c148ef0b928 100644 --- a/packages/framework-base/src/component-patterns/README.md +++ b/packages/framework-base/src/component-patterns/README.md @@ -30,7 +30,7 @@ from the first phase. Preserve their `focusTargetRef` separately from the consumer ref, then compose it through the native slot in the render phase. The public `FocusTarget` reports requested versus confirmed focus and cancels requests on detach/disable; do not infer focus from calling a native method. -See the [desktop focus authoring guide](../../../../.github/skills/agentic-component-authoring/references/focus.md) +See the [desktop focus authoring guide](../../../../references/agents/focus/common.md) for target lifetime, platform activation, and renderer boundaries. | Need | Pattern | diff --git a/packages/framework/framework/AGENTS.md b/packages/framework/framework/AGENTS.md index 7b8fd690399..3124f2c8d9a 100644 --- a/packages/framework/framework/AGENTS.md +++ b/packages/framework/framework/AGENTS.md @@ -1,230 +1,21 @@ # v1 component framework -These instructions apply to `packages/framework/framework` and to components -authored with `@fluentui-react-native/framework`. They describe the repository's -v1 compose/compressible framework. Preserve this model when maintaining v1 code; -do not mix it with either legacy v0 or the newer framework-base component model. +These instructions apply to `packages/framework/framework`, which implements the repository's v1 `compose` and +`compressible` framework. The consumer recipe, recognition guide, and review checklist for v1 components live in the +shared [v1 reference](../../../references/agents/components/v1.md); read it before changing this package or any v1 +component. v1 is maintenance-only (R-V1-001). -## Recognizing v1 code +## Package boundaries -Treat a component as v1 when several of these are present: +- `compose` injects the Fluent `Theme` integration into `composeFactory` from `@fluentui-react-native/composition`. + Keep theme-agnostic factory work in that lower-level package. +- Styling and slots come from `use-styling`, `use-tokens`, and `use-slots`. Changes to factory, slot, token, or styling + semantics also require the matching package's tests. +- v1 is built on framework-base's compatibility `stagedComponent` path. Do not migrate it to the modern framework-base + patterns as part of maintenance (R-V1-002). +- Check `src/index.ts` before adding another public export, and keep exports explicit (R-REPO-001). -- imports of `compose`, `compressible`, `UseSlots`, `UseStylingOptions`, - `buildProps`, `buildUseTokens`, or `useFluentTheme` from - `@fluentui-react-native/framework`; -- a descriptor such as `ButtonType` with `props`, `tokens`, and `slotProps`; -- `compose({ slots, slotProps, tokens, states, useRender })`; -- a styling file whose tokens contain nested state layers such as `hovered`, - `pressed`, `disabled`, size, appearance, or shape; -- a public `V1` export beside a legacy unsuffixed export, for example - `ButtonV1`, `CheckboxV1`, `TextV1`, or `IconV1`. - -Representative compose components are -`packages/components/Button/src/Button.tsx`, -`packages/components/Checkbox/src/Checkbox.tsx`, -`packages/components/Avatar/src/Avatar.tsx`, and -`packages/components/Menu/src/MenuItem/MenuItem.tsx`. Representative -compressible components are `packages/components/Text/src/Text.tsx`, -`packages/components/Divider/src/Divider.tsx`, and -`packages/components/TabList/src/Tab/Tab.tsx`. - -Do not classify code by the JSX pragma alone. V0 and v1 components both use -`/** @jsxImportSource @fluentui-react-native/framework-base */` so callable slots -can render without extra wrappers. - -## Framework boundaries - -- **V0/legacy:** imports `@uifabricshared/foundation-compose` and uses - `settings`, `styles`, `usePrepareProps`, `render`, `mergeSettings`, - `_overrides`, and `_precedence`. Follow - `packages/deprecated/foundation-compose/AGENTS.md`; do not apply those shapes - to v1. -- **V1/this framework:** `compose` wraps - `packages/framework/composition`, while styling and slots come from - `use-styling`, `use-tokens`, and `use-slots`. Its component hook is - `useRender`, its customization input is a flat token object, and its state - layers are nested token objects. -- **Modern framework-base/agent-generation work:** `phasedComponent`, modern - `directComponent`, `Slot`, `OptionalSlot`, `ComponentProps`, and - public slot shorthand/`as` props are a different component-authoring model. - Framework-base utilities such as `mergeProps`, `useSlot`, `directComponent`, - and the JSX runtime may appear inside v1 implementations for compatibility; - that does not convert the component to the new model. - -Do not migrate between these frameworks during a maintenance change unless the -task explicitly requests a migration. In particular, v1 is built on the -compatibility `stagedComponent` path even though new framework-base components -must not add new staged components. - -## Framework model and public API - -`compose` injects the Fluent `Theme` integration into `composeFactory`. The -factory: - -1. resolves tokens and token-derived props through `buildUseStyling`; -2. creates all callable slots through `buildUseSlots`; -3. wraps `useRender` in a staged component; -4. attaches `.customize(...)`, `.compose(...)`, `displayName`, `__options`, and - any declared statics. - -Use the `@fluentui-react-native/framework` entry point from components. Import -`composeFactory` directly from `@fluentui-react-native/composition` only when -working on the theme-agnostic lower-level factory. - -- `.customize(...tokenSettings)` appends token layers. Inputs may be token - objects, theme functions, or component names. Define customized components at - module scope, not during render. -- `.compose(partialOptions)` deep-merges factory options and appends `tokens`. - Use it to replace slots, slot props, state declarations, or `useRender` - without adding a React wrapper. See the compose tests in Button and Checkbox. -- `buildUseTokens` and `buildUseStyling` are Fluent-theme-specialized wrappers - around the lower-level packages. `useFluentTheme` returns the context theme or - `defaultFluentTheme`. - -The package also re-exports established token builders, memo/merge helpers, -slot helpers, and theme types. Check `src/index.ts` before adding another public -export; keep exports explicit. - -## Compose component shape - -Keep the descriptor, styling, state hook, and renderer aligned: - -```tsx -interface ExampleType { - props: ExampleProps; - tokens: ExampleTokens; - slotProps: ExampleSlotProps; -} - -export const Example = compose({ - displayName: exampleName, - ...stylingSettings, - slots: { root: Pressable, content: Text }, - useRender: (userProps, useSlots) => { - const example = useExample(userProps); - const Slots = useSlots(userProps, (layer) => example.state[layer] || userProps[layer]); - - return (final, ...children) => { - const mergedProps = mergeProps(example.props, final); - return ( - - {children} - - ); - }; - }, -}); -``` - -`ComposeType` is available, but most current components use a named interface. -Only `props`, `slotProps`, `tokens`, and optional `statics` are extracted by the -factory. Extra descriptor fields such as `state` are documentation/type-sharing -conveniences, not factory-managed state. - -Typical files are `.types.ts`, `Tokens.ts` plus platform -variants, `.styling.ts`, `use.ts`, `.tsx`, -`index.ts`, tests/snapshots, and migration/spec docs where applicable. Preserve -existing v0/v1 export aliases and package API compatibility. - -## Slots and staged rendering - -- Keep every key in `slots`, `slotProps`, and the descriptor's `slotProps` - synchronized. Type ref-bearing roots with `React.PropsWithRef<...>`. -- Call `useSlots` once, unconditionally, in the hook stage. `buildUseSlots` - creates every slot with `useSlot`; conditional slot rendering belongs in the - returned hook-free render stage. -- Styled slot props are captured when slots are created. Props supplied in JSX - are merged on top. Merge prepared component props with final composition props - using `mergeProps`; do not mutate either object. -- Decide explicitly where children render. The traditional v1 continuation is - `(finalProps, ...children) => JSX`. Some transitional components return a - hook-free `directComponent(({ children, ...finalProps }) => JSX)` instead. - Never call hooks in either final render form. -- `filters` are supported by `use-slots`, but current v1 consumers rarely use - them. Preserve an existing filter contract; do not copy v0 - `{ slotType, filter }` slot descriptors into v1. -- Platform-only slots may be declared conditionally, as Button does for - `rippleContainer` and `focusInnerBorder`. Create slots consistently and render - them only on the matching platform. - -Use `compressible` for the established lighter-weight v1 pattern when a component -manually resolves tokens and creates slots rather than needing the full compose -options object. `compressible` also wraps a staged component and adds -`.customize(...)`; it does not expose `.compose(...)`. - -In a compressible component: - -1. call hooks, `useFluentTheme`, and `useTokens(theme)` in the outer function; -2. use `applyTokenLayers`, `applyPropsToTokens`, or `patchTokens` when state/props - must alter resolved tokens; -3. build all slots with `useSlot`; -4. return a hook-free continuation and merge final props there. - -Follow existing examples rather than calling `stagedComponent` directly. - -## Tokens, styling, and state precedence - -- Define defaults as stable objects or theme functions, then place the component - name last, for example - `tokens: [defaultButtonTokens, defaultButtonFontTokens, buttonName]`. The name - loads `theme.components[name]` overrides. -- Model visual states as nested values of the same token type. List them in - `states` in application order; later active layers win. -- Supply a lookup to `useSlots` when state is derived from interaction hooks, - enum props, context, content, or platform defaults. Button's `buttonLookup` - and Avatar's `avatarLookup` are representative. -- `tokensThatAreAlsoProps` is opt-in. After state layers are applied, listed - props override those token values. Prefer a precise key list over `'all'` to - retain caching and avoid leaking unrelated props into tokens. -- Build each slot's cached props with `buildProps`. List every token key read by - the builder, including helper key arrays such as `fontStyles.keys`, - `borderStyles.keys`, and `layoutStyles.keys`. -- Treat resolved tokens and cached slot props as immutable. Use `patchTokens` - rather than mutation for instance-dependent values in compressible components. -- Keep theme/default/state values in token files, token-to-native mapping in - styling files, and volatile handlers/accessibility/data/ref props in the - component state hook or render preparation. - -## Hooks, state, and refs - -Put controlled/uncontrolled state, context, interaction hooks, callbacks, -accessibility normalization, and ref preparation in `use` or the -outer `useRender`/compressible stage. Return a conventional `{ props, state }` -object when the component has derived behavior; `state` is component-owned data -used by the token lookup and final render. - -Preserve established `componentRef` APIs. Convert `componentRef` to the native -root `ref` during preparation, commonly with `useViewCommandFocus`, as Button, -Checkbox, and MenuItem do. Do not introduce `forwardRef` or replace -`componentRef` with a public `ref` as an incidental change. - -Hooks must run in a stable order before the continuation is returned. Never call -hooks inside state lookups, `buildProps` callbacks, conditional slot branches, -or the final render stage. - -## Platform-specific behavior - -Use React Native platform resolution and keep the public props/type/export -contract consistent across `.android`, `.ios`, `.macos`, `.mobile`, `.windows`, -and `.win32` implementations. Platform variants may replace the root with a -native component and use a one-slot compose implementation; see -`packages/experimental/Checkbox/src/Checkbox.macos.tsx` and -`packages/experimental/Avatar/src/NativeAvatar.tsx`. - -Keep platform defaults and token differences in platform token/styling files -when practical. Keep imports from React Native forks inside their platform files -and retain the repository's `Platform.OS === ('win32' as any)` convention. - -## Testing and validation - -Add or update the owning component's Jest tests and snapshots for rendering, -state layers, `.customize`, `.compose`, slot replacement, accessibility, and -platform behavior affected by a change. Review snapshot trees rather than -updating them blindly. The framework package's direct test covers compressible; -lower-level compose, slot, token, and styling behavior is tested in the -corresponding framework packages. - -Run the smallest relevant package checks: +## Validation ```bash yarn workspace @fluentui-react-native/framework build @@ -232,13 +23,6 @@ yarn workspace @fluentui-react-native/framework test yarn workspace @fluentui-react-native/framework lint ``` -For component changes, also build/test the owning package, for example: - -```bash -yarn workspace @fluentui-react-native/button build -yarn workspace @fluentui-react-native/button test -``` - -Changes to factory, slot, token, or styling semantics also require the matching -`@fluentui-react-native/composition`, `use-slots`, `use-tokens`, or -`use-styling` package tests. +The package's direct test covers `compressible`; lower-level compose, slot, token, and styling behavior is tested in +`@fluentui-react-native/composition`, `use-slots`, `use-tokens`, and `use-styling`. Also build and test affected +consumer components, such as `@fluentui-react-native/button`. diff --git a/references/agents/README.md b/references/agents/README.md new file mode 100644 index 00000000000..e8a3ec575e9 --- /dev/null +++ b/references/agents/README.md @@ -0,0 +1,123 @@ +--- +id: index +kind: index +owner_paths: + - references/agents +--- + +# Agent references + +Shared, tool-neutral knowledge for agents working in this repository. The root [AGENTS.md](../../AGENTS.md) and scoped +`AGENTS.md` files route here, and skills in [`.github/skills`](../../.github/skills) own task workflows. Read only the +documents a task needs; links are navigation, not instructions to load everything they reach. + +## Taxonomy + +| Area | Contents | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [`repository/`](repository) | Architecture, commands and validation, TypeScript, permissions, and release | +| [`design/`](design) | Tokens, theming, and color utilities | +| [`components/`](components) | Classification, v0 and v1 recipes, and legacy test apps | +| [`components/agentic/`](components/agentic) | Agentic component recipes | +| [`components/agentic/contract-sources/`](components/agentic/contract-sources) | Source adapters for agentic component contracts | +| [`focus/`](focus) | Desktop focus behavior across component generations | +| [`accessibility/`](accessibility) | Version-bound accessibility action evidence | +| [`native/`](native) | Native components, modules, and platform guidance | +| [`evals/`](evals/README.md) | Portable evaluation case fixtures; not instructions | + +## Document index + +| ID | Document | Read when | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `repository.architecture` | [Architecture](repository/architecture.md) | Orienting in the monorepo or choosing where code belongs | +| `repository.development` | [Commands and validation](repository/development.md) | Building, testing, linting, validating, or adding a package | +| `repository.typescript` | [TypeScript](repository/typescript.md) | Changing compiler settings, platform types, or the JSX runtime | +| `repository.permissions` | [Agent permissions](repository/permissions.md) | Using `gh api`, Pod, process, loopback, or capture operations; changing policy | +| `repository.release` | [Changesets and release](repository/release.md) | Changing a published package or release tooling | +| `repository.dependency-security` | [Dependency security procedures](repository/dependency-security.md) | Remediating dependency advisories with the `fix-security-issues` skill | +| `design.tokens-and-theming` | [Tokens and theming](design/tokens-and-theming.md) | Mapping tokens, theme-dependent styles, or color utilities | +| `components.classification` | [Classification and routing](components/classification.md) | Starting any component task | +| `components.v0` | [v0 maintenance](components/v0.md) | Changing or reviewing `foundation-compose` components | +| `components.v1` | [v1 maintenance](components/v1.md) | Changing or reviewing `compose` or `compressible` components | +| `components.legacy-test-apps` | [FluentTester and E2E](components/legacy-test-apps.md) | Changing v0 or v1 test pages or E2E specs | +| `components.agentic.types-and-slots` | [Types and slots](components/agentic/types-and-slots.md) | Agentic public props, slots, resolved state, native props, or exports | +| `components.agentic.state-and-accessibility` | [State and accessibility](components/agentic/state-and-accessibility.md) | Agentic state hooks, defaults, interaction, or accessibility | +| `components.agentic.styles-and-tokens` | [Styles and tokens](components/agentic/styles-and-tokens.md) | Agentic styles, token mapping, theme caching, or state precedence | +| `components.agentic.rendering` | [Rendering and assembly](components/agentic/rendering.md) | Agentic render functions, assembly, or display names | +| `components.agentic.tests-and-stories` | [Tests and stories](components/agentic/tests-and-stories.md) | Agentic tests, stories, `wdio` story tests, or validation | +| `components.agentic.contract-sources.flex-x3` | [Flex and X3](components/agentic/contract-sources/flex-x3.md) | Contract evidence from the pinned Flex catalog | +| `components.agentic.contract-sources.fluent-v9` | [Fluent v9](components/agentic/contract-sources/fluent-v9.md) | Contract evidence from Fluent UI v9 source or specifications | +| `components.agentic.contract-sources.html-css` | [HTML and CSS](components/agentic/contract-sources/html-css.md) | Contract evidence from HTML, CSS, or browser output | +| `components.agentic.contract-sources.visual-reference` | [Visual reference](components/agentic/contract-sources/visual-reference.md) | Contract evidence from images, screenshots, or renders | +| `components.agentic.contract-sources.local-foundation` | [Local foundation](components/agentic/contract-sources/local-foundation.md) | Foundational components without a Flex catalog entry | +| `components.agentic.contract-sources.composite-evidence` | [Composite evidence](components/agentic/contract-sources/composite-evidence.md) | Contracts combining more than one source set | +| `focus.common` | [Common focus authoring](focus/common.md) | Any focusable component, collection, popup, focus hook, or native focus adapter | +| `focus.windows-win32` | [Windows and Win32 focus](focus/windows-win32.md) | Focus work on React Native Windows or Win32 | +| `focus.macos` | [macOS focus](focus/macos.md) | Focus work on macOS | +| `focus.platform-evidence` | [Desktop focus platform evidence](focus/platform-evidence.md) | Checking or updating a version-bound focus observation (`C-FOCUS-*`) | +| `accessibility.action-transport-evidence` | [Accessibility action transport evidence](accessibility/action-transport-evidence.md) | Checking or updating toggle/select transport observations (`C-A11Y-*`) | +| `native.components-and-modules` | [Native components and modules](native/components-and-modules.md) | Any native component, module, service, or native-backed wrapper | +| `native.apple-office` | [Apple native authoring and Office integration](native/apple-office.md) | Any iOS or macOS Swift, Objective-C, header, podspec, or bridge-manager change | +| `native.windows-fabric` | [Windows Fabric native components](native/windows-fabric.md) | React Native Windows Fabric views, codegen, registration, or UIA | + +## Placement and extension + +- Put guidance here when more than one skill, reviewer, scoped `AGENTS.md`, or evaluation uses it, or when it is general + repository knowledge. Keep guidance in a skill when only that workflow uses it. +- Keep product contracts (`SPEC.md`, `spec/`, `CONTRACT.md`), protocols, token maps, and package documentation with + their packages, and link to them. Owner-local instruction trees, such as the desktop driver's `references/`, stay + with their packages. +- Place a new document in the narrowest existing area. Create a new area only for a distinct domain, and add it to the + taxonomy table. Add every new document to the document index with a concrete "Read when" condition. +- When moving a document, keep its `id` and update every inbound link in the same change. Leave a + `compatibility-reference` at the old path only when known external links depend on it, and link it directly to the + new location. + +## Frontmatter standard + +Every Markdown file in this directory, and every Markdown file under a skill's `references/` directory, starts with +exactly these keys in this order: + +```yaml +--- +id: components.v1 +kind: instruction-reference +owner_paths: + - packages/framework/framework +--- +``` + +- `id`: lowercase, dot-separated segments of letters, digits, and hyphens. IDs are unique and permanent, including + across moves. +- `kind`: `index` for directory indexes, `instruction-reference` for guidance agents follow, `evidence-reference` for + version-bound observations, or `compatibility-reference` for a redirect left at a retired path. +- `owner_paths`: one or more existing repository paths whose maintainers own the technical content. + +Do not add other keys. Load conditions belong in this index and in routers, not in frontmatter. + +## Rule and claim IDs + +- `R--` identifies a consequential, testable obligation, such as `R-V1-003`. Define it once, in an + instruction file, as a list item that begins with the bold ID and a colon. Elsewhere, cite the ID. +- `C--` identifies a version-bound observation. Its definition states the source, inspected version or + revision, and date. +- Assign IDs only where an implementation, review, or evaluation needs to cite the obligation. Never renumber or reuse + an ID; retire it by removing its definition and every citation. +- These IDs cannot collide with component requirement IDs such as `BTN-006`, which belong to component contracts. + +## Routing conventions + +- State a prerequisite explicitly: "Read [common focus authoring](focus/common.md) first", a sentence such as "Focus + work starts with" followed by the linked document, or a `Read first` section. Prerequisites must not form cycles, and + a chain may be at most three reads deep. +- Compatibility references are terminal. Link to the replacement document, never to a compatibility path. +- Routing tables, meaning tables with a `Skill` or `Workflow` column, route each task to exactly one skill. A task + routed in more than one table must route to the same skill everywhere. Overlays belong in the workflow, not the + routing table. +- Route every project skill from the root `AGENTS.md` routing table. + +## Validation + +`yarn check-agent-instructions` validates frontmatter, links and fragments in instruction files, inbound fragments to +instruction files, skill metadata, rule definitions and citations, this index, the routing conventions above, and the +evaluation fixtures. It also runs in `yarn lage buildci`. diff --git a/references/agents/accessibility/action-transport-evidence.md b/references/agents/accessibility/action-transport-evidence.md new file mode 100644 index 00000000000..3400b092a4a --- /dev/null +++ b/references/agents/accessibility/action-transport-evidence.md @@ -0,0 +1,33 @@ +--- +id: accessibility.action-transport-evidence +kind: evidence-reference +owner_paths: + - packages/framework-base/src/accessibility +--- + +# Accessibility action transport evidence + +Version-bound observations behind the toggle and select transport contract in the +[framework-base accessibility instructions](../../../packages/framework-base/src/accessibility/AGENTS.md). Installed +paths are relative to each platform package's root. Recheck these claims when a pinned renderer version changes. + +- **C-A11Y-001:** React Native Windows Fabric dispatches lowercase `toggle` and `select` action names from + `Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp`. + `UiaHelpers.cpp::DispatchAccessibilityAction` compares names case-sensitively and emits once for every matching + declaration, so duplicate declarations produce duplicate callbacks. Version: `react-native-windows` 0.81.35. Observed: + 2026-09-17 (PR #4314). +- **C-A11Y-002:** Office Win32 declares the UIA action names `Toggle` and `Select` in + `Libraries/Components/View/ViewAccessibility.d.ts`, and FURN V1 Checkbox and Switch use `Toggle`. Version: + `@office-iss/react-native-win32` 0.81.8. Observed: 2026-09-17 (PR #4314). Limits: these are type declarations; + native UIA invocation still needs target-platform verification. +- **C-A11Y-003:** React Native macOS Fabric exposes declared custom action names verbatim through + `accessibilityCustomActions` in `React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm`, constructs them + from `name` while ignoring `label`, and emits the exact name from `didActivateAccessibilityCustomAction`. Its + `accessibilityActivate` only forwards `onAccessibilityTap`. Version: `react-native-macos` 0.81.9. Observed: 2026-09-17 + (PR #4314). Limits: these are custom actions, not an `AXPress` mapping. +- **C-A11Y-004:** React Native macOS Paper maps a custom action's label back to its declared name and implements + `accessibilityPerformPress` with `activate` or `onAccessibilityTap` in `React/Views/RCTView.m`. Version: + `react-native-macos` 0.81.9. Observed: 2026-09-17 (PR #4314). Limits: Paper behavior is not evidence for Fabric. +- **C-A11Y-005:** The React Native macOS Fabric View does not project `accessibilityState.disabled` or `selected` to + AXEnabled or AXSelected. Version: `react-native-macos` 0.81.9. Observed: 2026-09-17 (PR #4314). Limits: revisit + when the renderer is upgraded. diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/composite-evidence.md b/references/agents/components/agentic/contract-sources/composite-evidence.md similarity index 92% rename from .github/skills/agentic-component-contract-authoring/references/sources/composite-evidence.md rename to references/agents/components/agentic/contract-sources/composite-evidence.md index 984e3a534a0..8063da57e16 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/composite-evidence.md +++ b/references/agents/components/agentic/contract-sources/composite-evidence.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.composite-evidence +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Composite evidence adapter Use multiple `sources[]` entries when no single source can establish the full diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md b/references/agents/components/agentic/contract-sources/flex-x3.md similarity index 89% rename from .github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md rename to references/agents/components/agentic/contract-sources/flex-x3.md index 5d44e024b87..81ea257b3c5 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/flex-x3.md +++ b/references/agents/components/agentic/contract-sources/flex-x3.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.flex-x3 +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Flex and X3 source adapter Use this adapter for a component in the pinned Flex catalog. Flex skills are @@ -27,7 +34,8 @@ reviewer's assertion and remains empty after a source identity changes. ## Adapt to React Native Classify source guidance as adopted, adapted, intentionally divergent, not -applicable, or deferred. Consult the Flex token map and the closest Win32, +applicable, or deferred. Consult the +[Flex token map](../../../../../packages/agentic/design/src/tokens/mappings/flex-token-map.yaml) and the closest Win32, macOS, or Windows FURN implementation for compatibility; do not use iOS as the cross-platform canonical reference unless the work targets iOS. diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/fluent-v9.md b/references/agents/components/agentic/contract-sources/fluent-v9.md similarity index 91% rename from .github/skills/agentic-component-contract-authoring/references/sources/fluent-v9.md rename to references/agents/components/agentic/contract-sources/fluent-v9.md index 763e73abf53..72015fe93b0 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/fluent-v9.md +++ b/references/agents/components/agentic/contract-sources/fluent-v9.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.fluent-v9 +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Fluent v9 source adapter Use Fluent UI v9 as API, behavior, and design precedent, not as a DOM contract diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/html-css.md b/references/agents/components/agentic/contract-sources/html-css.md similarity index 92% rename from .github/skills/agentic-component-contract-authoring/references/sources/html-css.md rename to references/agents/components/agentic/contract-sources/html-css.md index 264c3cdda74..8c805fec993 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/html-css.md +++ b/references/agents/components/agentic/contract-sources/html-css.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.html-css +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # HTML and CSS source adapter Use HTML/CSS evidence to recover observable anatomy, content flow, layout, diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/local-foundation.md b/references/agents/components/agentic/contract-sources/local-foundation.md similarity index 89% rename from .github/skills/agentic-component-contract-authoring/references/sources/local-foundation.md rename to references/agents/components/agentic/contract-sources/local-foundation.md index 24c4cef1a28..f66774eaeb7 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/local-foundation.md +++ b/references/agents/components/agentic/contract-sources/local-foundation.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.local-foundation +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Local foundation source adapter Use `local-foundation` only for a theme-aware foundational component that has diff --git a/.github/skills/agentic-component-contract-authoring/references/sources/visual-reference.md b/references/agents/components/agentic/contract-sources/visual-reference.md similarity index 92% rename from .github/skills/agentic-component-contract-authoring/references/sources/visual-reference.md rename to references/agents/components/agentic/contract-sources/visual-reference.md index 8e92ee7084a..b7618b8bcec 100644 --- a/.github/skills/agentic-component-contract-authoring/references/sources/visual-reference.md +++ b/references/agents/components/agentic/contract-sources/visual-reference.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.contract-sources.visual-reference +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Visual reference source adapter Use screenshots, design renders, and other images as visual evidence only. diff --git a/.github/skills/agentic-component-authoring/references/rendering.md b/references/agents/components/agentic/rendering.md similarity index 96% rename from .github/skills/agentic-component-authoring/references/rendering.md rename to references/agents/components/agentic/rendering.md index 4bf1c6dce28..ce160a71704 100644 --- a/.github/skills/agentic-component-authoring/references/rendering.md +++ b/references/agents/components/agentic/rendering.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.rendering +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Rendering and assembly Use this reference for `render.tsx`, `.ts`, slot ordering, conditional structure, display names, diff --git a/.github/skills/agentic-component-authoring/references/state-and-accessibility.md b/references/agents/components/agentic/state-and-accessibility.md similarity index 97% rename from .github/skills/agentic-component-authoring/references/state-and-accessibility.md rename to references/agents/components/agentic/state-and-accessibility.md index 69b977c37a0..0090d1bed66 100644 --- a/.github/skills/agentic-component-authoring/references/state-and-accessibility.md +++ b/references/agents/components/agentic/state-and-accessibility.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.state-and-accessibility +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # State and accessibility Use this reference for `StateProps`, `use.ts`, defaults, interaction hooks, accessibility, derived @@ -167,8 +174,8 @@ so another component can extend the state stage without importing package intern ## Platform behavior For focus ownership, activation, and native target lifetime, read -[common focus authoring](focus.md), followed by the -[Windows/Win32](focus-windows.md) or [macOS](focus-macos.md) detail. +[common focus authoring](../../focus/common.md), followed by the +[Windows/Win32](../../focus/windows-win32.md) or [macOS](../../focus/macos.md) detail. Keep platform-specific native imports out of shared files when React Native forks expose incompatible types. Put fork imports in platform files or redeclare a small platform-neutral contract. Surface unsupported platform behavior rather diff --git a/.github/skills/agentic-component-authoring/references/styles-and-tokens.md b/references/agents/components/agentic/styles-and-tokens.md similarity index 98% rename from .github/skills/agentic-component-authoring/references/styles-and-tokens.md rename to references/agents/components/agentic/styles-and-tokens.md index f2334c1ce53..3174ba3be58 100644 --- a/.github/skills/agentic-component-authoring/references/styles-and-tokens.md +++ b/references/agents/components/agentic/styles-and-tokens.md @@ -1,3 +1,11 @@ +--- +id: components.agentic.styles-and-tokens +kind: instruction-reference +owner_paths: + - packages/agentic/components + - packages/agentic/design +--- + # Styles and tokens Use this reference for `.styles.ts`, `useStyles.ts`, token mapping, theme caching, state precedence, and @@ -121,7 +129,7 @@ content slot. ## Share focus behavior and styling -Follow [common focus authoring](focus.md) and its Windows/Win32 or macOS detail. +Follow [common focus authoring](../../focus/common.md) and its Windows/Win32 or macOS detail. State hooks use `useFocusVisuals` to create a private optional `FocusRing` and determine the native `enableFocusRing` setting. The custom slot is absent on the native path; when present, its configured Views remain mounted across visibility changes. diff --git a/.github/skills/agentic-component-authoring/references/tests-and-stories.md b/references/agents/components/agentic/tests-and-stories.md similarity index 97% rename from .github/skills/agentic-component-authoring/references/tests-and-stories.md rename to references/agents/components/agentic/tests-and-stories.md index 93aeb2494bd..092c5ec96fa 100644 --- a/.github/skills/agentic-component-authoring/references/tests-and-stories.md +++ b/references/agents/components/agentic/tests-and-stories.md @@ -1,3 +1,11 @@ +--- +id: components.agentic.tests-and-stories +kind: instruction-reference +owner_paths: + - packages/agentic/components + - apps/storybook +--- + # Tests and stories Use this reference for runtime coverage, type coverage, visual snapshots, Storybook CSF, and validation. Canonical examples @@ -87,7 +95,7 @@ fixed demonstration values inside the story render function and expose only mean controls for React elements are noisy and cannot safely edit the contract. Story controls follow the axis's ownership, described in -[State and accessibility](./state-and-accessibility.md#decide-who-owns-a-stateful-axis). +[State and accessibility](state-and-accessibility.md#decide-who-owns-a-stateful-axis). For a self-driving control such as Checkbox, Switch, or Accordion, do not put the controlled prop in `args`. A pinned `checked`, `expanded`, or `status` arg forces every instance into externally driven mode, so pressing the component does @@ -217,8 +225,9 @@ a physical click does not exercise UIA/AX action dispatch. Report passed, failed, and skipped counts together. A zero-exit smoke traversal with platform-skipped focus cases is not cross-platform focus qualification. -The pinned RNmacOS 0.81.9 Fabric View does not project -`accessibilityState.disabled`/`selected` to AXEnabled/AXSelected. Those native +The macOS Fabric View does not project disabled or selected accessibility state +to AXEnabled/AXSelected (C-A11Y-005 in the +[action transport evidence](../../accessibility/action-transport-evidence.md)). Those native announcement assertions are isolated in explicitly skipped cases; shared focus, activation, and selection-callback cases still run. Do not substitute rendered state for an AX result or describe these skips as intended macOS diff --git a/.github/skills/agentic-component-authoring/references/types-and-slots.md b/references/agents/components/agentic/types-and-slots.md similarity index 96% rename from .github/skills/agentic-component-authoring/references/types-and-slots.md rename to references/agents/components/agentic/types-and-slots.md index 9b80c2b30be..fffd0ccbaf3 100644 --- a/.github/skills/agentic-component-authoring/references/types-and-slots.md +++ b/references/agents/components/agentic/types-and-slots.md @@ -1,3 +1,10 @@ +--- +id: components.agentic.types-and-slots +kind: instruction-reference +owner_paths: + - packages/agentic/components +--- + # Types and slots Use this reference when defining or changing a component's public API, slots, resolved state, native prop exposure, or @@ -56,7 +63,7 @@ when rendering the slot so Framework Base composes them. Never overwrite the con unrelated native instance types, omit a top-level ref unless it can define one sound, stable imperative contract. For focus targets, attachment generations, callback cleanup, and view-command -boundaries, follow [common focus authoring](focus.md#modern-refs-and-slot-composition). +boundaries, follow [common focus authoring](../../focus/common.md#modern-refs-and-slot-composition). Modern ref props do not imply that a structural root is the keyboard focus target or that a native focus command has completed. diff --git a/references/agents/components/classification.md b/references/agents/components/classification.md new file mode 100644 index 00000000000..5da9c196fe0 --- /dev/null +++ b/references/agents/components/classification.md @@ -0,0 +1,80 @@ +--- +id: components.classification +kind: instruction-reference +owner_paths: + - packages/components + - packages/agentic/components + - packages/framework + - packages/deprecated/foundation-compose +--- + +# Component classification and routing + +Classify every changed surface before choosing a workflow or recipe. A single package, or a single diff, can contain +more than one generation; Button ships both v0 and v1 implementations. + +- **R-CLASSIFY-001:** Classify an implementation by its imports and component structure together. Paths such as + `deprecated/` or `legacy/`, export suffixes, the JSX pragma, and individual helpers such as `directComponent` or + `stagedComponent` are hints, not proof. +- **R-CLASSIFY-002:** Record the task mode, implementation model, public compatibility surface, and native obligations + separately for each changed surface. Load each applicable recipe for a mixed change; never force the whole change + into one generation. + +## 1. Task mode + +| Task | Workflow | +| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| Create a new component | [agentic-component-authoring](../../../.github/skills/agentic-component-authoring/SKILL.md) | +| Change, or plan a change to, an existing agentic component or primitive | [agentic-component-authoring](../../../.github/skills/agentic-component-authoring/SKILL.md) | +| Agentic contract-only or contract-feasibility work | [agentic-component-contract-authoring](../../../.github/skills/agentic-component-contract-authoring/SKILL.md) | +| Fix or maintain an existing v0 or v1 component | [maintain-v0-v1-component](../../../.github/skills/maintain-v0-v1-component/SKILL.md) | +| Review a component change of any generation | [review-component](../../../.github/skills/review-component/SKILL.md) | +| Native component, TurboModule, native service, codegen, or native-core `/legacy` wrapper change | [implement-native-component](../../../.github/skills/implement-native-component/SKILL.md) | +| Cross-component agentic extraction or dependency direction | [agentic-component-optimization](../../../.github/skills/agentic-component-optimization/SKILL.md) | +| Storybook app, Metro, CocoaPods, or desktop native toolchain | [agentic-storybook-development](../../../.github/skills/agentic-storybook-development/SKILL.md) | +| Dependency vulnerability remediation | [fix-security-issues](../../../.github/skills/fix-security-issues/SKILL.md) | + +v0 and v1 are maintenance-only (R-V0-001, R-V1-001). A request for a new component uses the agentic workflow even +when a v0 or v1 component with a similar name exists. + +## 2. Implementation model + +| Model | Decisive evidence | Representative source | Recipe | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| v0 | Imports from `@uifabricshared/foundation-compose` (or `foundation-settings`/`foundation-tokens`); `IComposeSettings`, `usePrepareProps`, `mergeSettings`, `_overrides`, `_precedence` | [deprecated Button](../../../packages/components/Button/src/deprecated/Button.tsx) | [v0](v0.md) | +| v1 compose | `compose` from `@fluentui-react-native/framework` with a `{ props, tokens, slotProps }` descriptor and `useRender` | [Button](../../../packages/components/Button/src/Button.tsx) | [v1](v1.md) | +| v1 compressible | `compressible` from `@fluentui-react-native/framework`; it may return a framework-base `directComponent` continuation | [Text](../../../packages/components/Text/src/Text.tsx) | [v1](v1.md) | +| Agentic higher-order | Under `packages/agentic/components/src/components`; `.types.ts`, `use.ts`, `useStyles.ts`, `render.tsx`, and `.ts`; `_unstable` stage exports; React 19 `ref` prop | [agentic Button](../../../packages/agentic/components/src/components/button/button.ts) | [agentic skill](../../../.github/skills/agentic-component-authoring/SKILL.md) | +| Agentic primitive | Under `packages/agentic/components/src/primitives`; colocated `CONTRACT.md`; unstyled; often a hook-free `directComponent` | [Icon](../../../packages/agentic/components/src/primitives/icon/icon.tsx) | [primitive instructions](../../../packages/agentic/components/src/primitives/AGENTS.md) | +| Framework-base pattern | `phasedComponent`, modern `directComponent`, `useSlot`/`useOptionalSlot`, and `Slot` outside the agentic package | [component patterns](../../../packages/framework-base/src/component-patterns/README.md) | [framework-base instructions](../../../packages/framework-base/AGENTS.md) | +| No component model | Native modules, services, utilities, or tooling | [native-core](../../../packages/agentic/native-core/README.md) | Owning package documentation | + +Framework-base utilities such as `mergeProps`, `useSlot`, `directComponent`, and the JSX runtime appear inside v1 and +v0 implementations for compatibility. Their presence does not change the component's generation. Both v0 and v1 use +the `@jsxImportSource @fluentui-react-native/framework-base` pragma. + +## 3. Public compatibility surface + +Record the API obligations independently of the implementation model: + +- Legacy unsuffixed exports beside `V1` exports, such as `Button` and `ButtonV1`. +- v0 and v1 `componentRef` APIs, versus the agentic React 19 `ref` prop. +- The agentic package root versus its `@fluentui-react-native/components/primitives` entrypoint. +- Native-core's `/legacy` entrypoint. Its [Callout](../../../packages/agentic/native-core/src/legacy/callout/Callout.tsx) + is a modern implementation that preserves a legacy public API and `componentRef`. +- Native registration names, which must not change during maintenance. + +## 4. Native obligations + +Treat the change as native work, and add the +[implement-native-component](../../../.github/skills/implement-native-component/SKILL.md) checklist as an overlay, +when it touches any of: + +- Swift, Objective-C, Objective-C++, C++, IDL, headers, podspecs, or Windows project files; +- `codegenNativeComponent` or TurboModule specifications, native registration, or native event/command shapes; +- a platform wrapper whose root is a native component, such as + [the macOS experimental Checkbox](../../../packages/experimental/Checkbox/src/Checkbox.macos.tsx); +- accessibility actions, focus targets, or refs that cross a native boundary. + +File suffixes are not the only trigger: a TypeScript-only change to a native prop, ref, or event contract is native +work. diff --git a/references/agents/components/legacy-test-apps.md b/references/agents/components/legacy-test-apps.md new file mode 100644 index 00000000000..004901ffb52 --- /dev/null +++ b/references/agents/components/legacy-test-apps.md @@ -0,0 +1,38 @@ +--- +id: components.legacy-test-apps +kind: instruction-reference +owner_paths: + - apps/fluent-tester + - apps/tester-core + - apps/E2E + - apps/win32 +--- + +# FluentTester and E2E coverage for v0 and v1 components + +v0 and v1 components are exercised manually in FluentTester and automatically with Appium and WebdriverIO. Agentic +components use Storybook stories and `wdio` story tests instead; see +[tests and stories](agentic/tests-and-stories.md). + +## FluentTester + +- [`apps/fluent-tester`](../../../apps/fluent-tester/README.md) hosts the tester on Android, iOS, macOS, and Windows; + [`apps/win32`](../../../apps/win32) hosts it on Win32. +- Test pages live in `apps/tester-core/src/TestComponents//`. Register each page in + [`apps/tester-core/src/testPages.ts`](../../../apps/tester-core/src/testPages.ts), whose `platforms` array declares + where it appears. +- A page that supports E2E tests sets the first section's `testID` to the page object's `_pageName` and may supply + dedicated `e2eSections`, as [ButtonTest](../../../apps/tester-core/src/TestComponents/Button/ButtonTest.tsx) does. + +## End-to-end tests + +- Tests live in `apps/E2E/src//`: a page object under `pages/` (for example + [ButtonV1PageObject](../../../apps/E2E/src/ButtonV1/pages/ButtonV1PageObject.ts)), platform specs under `specs/` (for + example `ButtonV1.spec.win.ts`), and shared constants in `consts.ts` where needed. +- Run them with the E2E workspace's declared scripts, such as `yarn e2etest:` from `apps/E2E`, after the + matching `e2eprep:` setup. +- Android selectors use `accessibilityLabel`; other platforms use `testID`. +- Win32 automation uses WinAppDriver. + +When a maintenance change alters observable behavior, update the existing page and spec rather than creating a +parallel test app. Report E2E runs that were not executed, and why, rather than implying coverage (R-VALID-002). diff --git a/references/agents/components/v0.md b/references/agents/components/v0.md new file mode 100644 index 00000000000..3ff59171868 --- /dev/null +++ b/references/agents/components/v0.md @@ -0,0 +1,190 @@ +--- +id: components.v0 +kind: instruction-reference +owner_paths: + - packages/deprecated/foundation-compose + - packages/deprecated/foundation-settings + - packages/deprecated/foundation-tokens +--- + +# v0 component maintenance + +Use this recipe for components built on `@uifabricshared/foundation-compose`. Confirm the generation with the +[classification guide](classification.md) first; v0 and v1 can live in the same package. + +- **R-V0-001:** v0 is maintenance-only. Do not create a v0 component unless the task explicitly requires v0 + compatibility, and do not migrate v0 code to v1, framework-base, or agentic patterns as part of maintenance. +- **R-V0-002:** Preserve settings layering (`mergeSettings`, the settings-name entry, `_overrides`, and + `_precedence`), slot prop filters, token key lists, and the split between preparation and rendering. Each + participates in compatibility, composition, or caching. +- **R-V0-003:** Preserve existing `componentRef` APIs. Do not introduce `forwardRef` or a new public `ref` + convention without an explicit migration requirement. + +## Recognize v0 code + +v0 code imports from `@uifabricshared/foundation-compose`, `foundation-composable`, `foundation-settings`, or +`foundation-tokens`, and typically has: + +- a descriptor such as `IButtonType` with `props`, `tokens`, `slotProps`, optional `state`, and optional `statics`; +- `compose({...})`, `IUseComposeStyling`, `ISlots<...>`, and `mergeSettings(...)`; +- settings shaped as `{ tokens, root, ...slots, _overrides, _precedence }`; +- an old unsuffixed export beside a newer `V1` export. + +A `deprecated/` or `legacy/` directory is only a hint: native-core's legacy Callout lives under `legacy/` but uses +modern helpers. Representative v0 implementations are +[deprecated Button](../../../packages/components/Button/src/deprecated/Button.tsx), +[deprecated Checkbox](../../../packages/components/Checkbox/src/deprecated/Checkbox.tsx), +[legacy Link](../../../packages/components/Link/src/legacy/Link.tsx), +[Persona](../../../packages/components/Persona/src/Persona.tsx), and +[Stack](../../../packages/components/Stack/src/Stack.tsx). + +## Framework model and public API + +`compose` combines the lower-level composable, themed-settings, settings, and token systems. Its phases are: + +1. resolve and cache layered `settings`; +2. resolve token props into cached slot props and styles through `styles`; +3. run `usePrepareProps` for hooks, state, refs, and instance-specific props; +4. call the hook-free `render` function with prepared `Slots`. + +The package publicly exports `compose`, `initializeStyling`, and the compose option, return, settings, styling, and +type-extraction types listed in its `src/index.ts`. The returned component exposes `__composable`, +`.compose(options)`, and `.customize(...settings)`. Prefer the strongly typed component methods: + +- `.compose(...)` replaces or augments behavior, slots, styles, settings, or render without adding a React wrapper. + Settings are appended to the base settings. See + [PrimaryButton](../../../packages/components/Button/src/deprecated/PrimaryButton/PrimaryButton.ts). +- `.customize(...)` only appends old-style settings entries. Its input retains `tokens`, `_overrides`, `_precedence`, + and slot-prop keys; it is not the v1 flat token customization format. +- `statics` attaches subcomponents, as in `Stack.Item`. + +The exported two-argument `compose(options, base)` form is the underlying API; use `Base.compose(options)` when the +base is already a typed composed component. `initializeStyling` is infrastructure that `compose` installs +automatically, not something component authors normally call. + +## Component shape + +Define a shared descriptor type and keep slot names aligned: + +```ts +interface IExampleType { + props: IExampleProps; + tokens: IExampleTokens; + slotProps: IExampleSlotProps; // must contain root + state: IExampleState; +} + +export const Example = compose({ + displayName: exampleName, + settings, + usePrepareProps, + render, + slots: { root: View, content: Text }, + styles: { root: [backgroundColorTokens], content: [textTokens] }, +}); +``` + +Typical files are `.types.ts`, `.settings.ts`, `.tokens*.ts`, `.tsx`, +`index.ts`, and colocated Jest tests and snapshots. Preserve existing public `I`-prefixed types, deprecated exports, +display names, prop names, and v0/v1 aliases. + +For a one-slot component, `compose` supplies default preparation and rendering: it applies styling and user props to +`root`. See [deprecated Text](../../../packages/components/Text/src/deprecated/Text.tsx) and +[StackItem](../../../packages/components/Stack/src/StackItem/StackItem.tsx). Multiple slots require an explicit +`render`. Once custom `usePrepareProps` is supplied, it must put the public and native props on the appropriate slot; +the framework does not do that automatically. + +## Slots and rendering + +- `slotProps` is a named props collection with a required `root`. +- Define each slot as a component or string, or as `{ slotType, filter }`. Use existing filters such as + `filterViewProps`, `filterTextProps`, and `filterImageProps` so component-only and token props do not reach native + views. +- Merge prepared props with styling, normally `mergeSettings(useStyling(userProps, lookup), { root: rootProps, ... })`. + Later entries win, including merged styles and class names. +- Render prepared slots as `` and ``. Their prepared props are already captured; pass only + additional render-time props in JSX. +- Children are removed before preparation and supplied to `render` as `...children`. Decide explicitly where they + belong, as Button, Link, and Stack do. Conditional slot rendering is safe because all hooks already ran. +- Put `/** @jsxImportSource @fluentui-react-native/framework-base */` first in files that render callable `Slots` with + JSX. This pragma supports the legacy slot runtime; it does not make the component a framework-base pattern. + +## Settings, tokens, and styling + +- Type settings as `IComposeSettings`. Entries are layered in array order and may be a settings object, a + theme settings name string, or a `(theme) => settings` function. Keep objects and functions stable and never mutate + cached settings. +- Use `_precedence` to define state application order and `_overrides` for state-specific settings; nested overrides + are supported. Settings names such as `buttonName` load theme-provided component settings. +- `useStyling(props)` uses props as the default override mask. For derived interaction state, pass an override lookup + function or mask, as in legacy Button, Checkbox, and Link. +- Declare token-to-slot processing in `styles`. Reuse operation sets such as `backgroundColorTokens`, `borderTokens`, + `foregroundColorTokens`, and `textTokens`; use `{ source, target, lookup }` for mappings and theme lookup. +- For component-specific calculations, use `styleFunction` and list every token key that affects the result. See + [Persona root tokens](../../../packages/components/Persona/src/Persona.tokens.root.ts), + [Persona text tokens](../../../packages/components/Persona/src/Persona.tokens.texts.ts), and + [Stack tokens](../../../packages/components/Stack/src/Stack.tokens.ts). Accurate key lists are required for + memoization. +- When a slot is itself composed and supports a token, token processing can forward that token to the child instead of + producing a parent-owned style. Preserve compatible child token contracts when replacing composed slots. +- Put theme, default, and state values in settings and token-derived style in `styles`. Put highly variable values + such as text, accessibility props, handlers, sources, and refs into `usePrepareProps` slot props. + +## State, hooks, and refs + +- `usePrepareProps` and `useStyling` are hook phases: call them on every render and never conditionally. Hooks, + context, controlled and uncontrolled state, and interaction hooks belong in `usePrepareProps`. +- Return `{ slotProps, state }`; `state` is the typed payload for `render`, not necessarily React state. Keep `render` + hook-free. +- Convert `componentRef` to the native root `ref` during preparation, commonly with `useViewCommandFocus`, and type + ref-bearing roots with `React.PropsWithRef<...>`. For imperative native APIs, use an internal native ref plus + `useImperativeHandle`; native-core's [legacy Callout](../../../packages/agentic/native-core/src/legacy/callout/Callout.tsx) + shows that pattern behind a legacy API. + +## Platform-specific implementations + +Use React Native resolution for `.android.tsx`, `.macos.tsx`, `.windows.tsx`, and `.win32.tsx`, and for platform +settings files. Keep the same public descriptor and props across variants while allowing different native slots and +preparation logic, as deprecated Button on Android and RadioButton on macOS and Win32 do. Keep imports from platform +React Native forks inside their platform files (R-REPO-002). A platform file whose root is a native component also +needs the native overlay from the [classification guide](classification.md#4-native-obligations). + +## Maintenance boundaries + +- Do not rewrite v0 code to `@fluentui-react-native/composition`, `@fluentui-react-native/framework`, or framework-base + `phasedComponent`, `directComponent`, or `useSlot` patterns as part of maintenance. +- Do not apply v1 slot types, flat state tokens, `useTokens`, or the v1 `.customize({ token: value, state: {...} })` + shape to v0 code. +- Prefer surgical fixes. Migration to v1 is a separate task and must account for documented prop, token, slot, and + export differences such as [Button's migration guide](../../../packages/components/Button/MIGRATION.md). + +## Validation + +Run the owning packages' declared scripts. For the framework itself: + +```bash +yarn workspace @uifabricshared/foundation-compose build +yarn workspace @uifabricshared/foundation-compose lint +yarn workspace @uifabricshared/foundation-compose test +``` + +For behavior changes, also build and test the consumer package, for example: + +```bash +yarn workspace @fluentui-react-native/button build +yarn workspace @fluentui-react-native/button test +``` + +Update snapshots only after reviewing the rendered slot tree. Changes to settings override or merge semantics, or to +token processing, also require the `@uifabricshared/foundation-settings` or `@uifabricshared/foundation-tokens` tests. +Manual and end-to-end coverage for v0 components uses the [legacy test apps](legacy-test-apps.md). + +## Review checklist + +- Classification is supported by imports and structure, not a directory name (R-CLASSIFY-001). +- The change is maintenance, not an unrequested migration or new v0 component (R-V0-001). +- Settings layers, `_overrides`, `_precedence`, filters, and token key lists remain intact (R-V0-002). +- `componentRef`, deprecated exports, `I`-prefixed types, and display names are unchanged (R-V0-003). +- Hooks run only in `usePrepareProps` and `useStyling`; `render` stays hook-free. +- Platform variants keep the same public descriptor; fork imports stay in platform files. +- Snapshot updates were reviewed rather than accepted blindly. diff --git a/references/agents/components/v1.md b/references/agents/components/v1.md new file mode 100644 index 00000000000..a82803689af --- /dev/null +++ b/references/agents/components/v1.md @@ -0,0 +1,216 @@ +--- +id: components.v1 +kind: instruction-reference +owner_paths: + - packages/framework/framework + - packages/framework/composition + - packages/framework/use-slots + - packages/framework/use-tokens + - packages/framework/use-styling +--- + +# v1 component maintenance + +Use this recipe for components built on `@fluentui-react-native/framework` `compose` or `compressible`. Confirm the +generation with the [classification guide](classification.md) first. + +- **R-V1-001:** v1 is maintenance-only. Do not create new v1 components; new components use the + [agentic workflow](../../../.github/skills/agentic-component-authoring/SKILL.md). Maintain existing v1 components in + place. +- **R-V1-002:** Preserve the compose or compressible model. Do not mix it with v0 or framework-base component patterns, + or migrate between frameworks, unless the task explicitly requests a migration. Framework-base helpers used inside + v1 do not change its classification. +- **R-V1-003:** Preserve established `componentRef` APIs. Convert `componentRef` to the native root `ref` during + preparation; do not introduce `forwardRef` or replace `componentRef` with a public `ref` as an incidental change. +- **R-V1-004:** Call hooks, including `useSlots`, once and unconditionally in the hook stage. Final render + continuations, state lookups, and `buildProps` callbacks must not call hooks. + +## Recognize v1 code + +v1 code typically has several of: + +- imports of `compose`, `compressible`, `UseSlots`, `UseStylingOptions`, `buildProps`, `buildUseTokens`, or + `useFluentTheme` from `@fluentui-react-native/framework`; +- a descriptor such as `ButtonType` with `props`, `tokens`, and `slotProps`; +- `compose({ slots, slotProps, tokens, states, useRender })`; +- a styling file whose tokens contain nested state layers such as `hovered`, `pressed`, `disabled`, size, appearance, + or shape; +- a public `V1` export beside a legacy unsuffixed export, for example `ButtonV1`, `CheckboxV1`, `TextV1`, or `IconV1`. + +Representative compose components are [Button](../../../packages/components/Button/src/Button.tsx), +[Checkbox](../../../packages/components/Checkbox/src/Checkbox.tsx), +[Avatar](../../../packages/components/Avatar/src/Avatar.tsx), and +[MenuItem](../../../packages/components/Menu/src/MenuItem/MenuItem.tsx). Representative compressible components are +[Text](../../../packages/components/Text/src/Text.tsx), [Divider](../../../packages/components/Divider/src/Divider.tsx), +and [Tab](../../../packages/components/TabList/src/Tab/Tab.tsx). + +## Framework boundaries + +- **v0:** imports `@uifabricshared/foundation-compose` and uses `settings`, `styles`, `usePrepareProps`, `render`, + `mergeSettings`, `_overrides`, and `_precedence`. Follow the [v0 recipe](v0.md); do not apply those shapes to v1. +- **v1:** `compose` wraps `packages/framework/composition`, while styling and slots come from `use-styling`, + `use-tokens`, and `use-slots`. Its component hook is `useRender`, its customization input is a flat token object, + and its state layers are nested token objects. +- **Framework-base and agentic:** `phasedComponent`, modern `directComponent`, `Slot`, `OptionalSlot`, + `ComponentProps`, and public slot shorthand or `as` props form a different model. v1 is built on the compatibility + `stagedComponent` path even though new framework-base code must not add staged components. + +## Framework model and public API + +`compose` injects the Fluent `Theme` integration into `composeFactory`. The factory: + +1. resolves tokens and token-derived props through `buildUseStyling`; +2. creates all callable slots through `buildUseSlots`; +3. wraps `useRender` in a staged component; +4. attaches `.customize(...)`, `.compose(...)`, `displayName`, `__options`, and any declared statics. + +Use the `@fluentui-react-native/framework` entry point from components. Import `composeFactory` directly from +`@fluentui-react-native/composition` only when working on the theme-agnostic lower-level factory. + +- `.customize(...tokenSettings)` appends token layers. Inputs may be token objects, theme functions, or component + names. Define customized components at module scope, not during render. +- `.compose(partialOptions)` deep-merges factory options and appends `tokens`. Use it to replace slots, slot props, + state declarations, or `useRender` without adding a React wrapper. See the compose tests in Button and Checkbox. +- `buildUseTokens` and `buildUseStyling` are Fluent-theme-specialized wrappers around the lower-level packages. + `useFluentTheme` returns the context theme or `defaultFluentTheme`. + +The framework package also re-exports established token builders, memo and merge helpers, slot helpers, and theme +types. Check its `src/index.ts` before adding another public export; keep exports explicit (R-REPO-001). + +## Compose component shape + +Keep the descriptor, styling, state hook, and renderer aligned: + +```tsx +interface ExampleType { + props: ExampleProps; + tokens: ExampleTokens; + slotProps: ExampleSlotProps; +} + +export const Example = compose({ + displayName: exampleName, + ...stylingSettings, + slots: { root: Pressable, content: Text }, + useRender: (userProps, useSlots) => { + const example = useExample(userProps); + const Slots = useSlots(userProps, (layer) => example.state[layer] || userProps[layer]); + + return (final, ...children) => { + const mergedProps = mergeProps(example.props, final); + return ( + + {children} + + ); + }; + }, +}); +``` + +`ComposeType` is available, but most components use a named interface. Only `props`, `slotProps`, `tokens`, and +optional `statics` are extracted by the factory. Extra descriptor fields such as `state` are documentation and +type-sharing conveniences, not factory-managed state. + +Typical files are `.types.ts`, `Tokens.ts` plus platform variants, `.styling.ts`, +`use.ts`, `.tsx`, `index.ts`, tests and snapshots, and `SPEC.md` or `MIGRATION.md` where +applicable. Packages use `jest.config.cjs` and `tsconfig.json`. Preserve existing v0/v1 export aliases and package API +compatibility; consumers may alias imports, for example `import { ButtonV1 as Button }`. + +## Slots and staged rendering + +- Keep every key in `slots`, `slotProps`, and the descriptor's `slotProps` synchronized. Type ref-bearing roots with + `React.PropsWithRef<...>`. +- `buildUseSlots` creates every slot with `useSlot`; conditional slot rendering belongs in the returned hook-free + render stage. Slot functions return `React.ReactElement | null`, so `Slots.root({}).props` is typed without + assertions. +- Styled slot props are captured when slots are created. Props supplied in JSX are merged on top. Merge prepared + component props with final composition props using `mergeProps`; do not mutate either object. +- Decide explicitly where children render. The traditional continuation is `(finalProps, ...children) => JSX` and + returns `FinalRender`. Some transitional components return a hook-free + `directComponent(({ children, ...finalProps }) => JSX)` instead. +- `filters` are supported by `use-slots` but rarely used by v1 consumers. Preserve an existing filter contract; do not + copy v0 `{ slotType, filter }` slot descriptors into v1. +- Platform-only slots may be declared conditionally, as Button does for `rippleContainer` and `focusInnerBorder`. + Create slots consistently and render them only on the matching platform. + +Use `compressible` for the lighter-weight pattern where a component resolves tokens and creates slots manually. +`compressible` also wraps a staged component and adds `.customize(...)`; it does not expose `.compose(...)`. In a +compressible component: + +1. call hooks, `useFluentTheme`, and `useTokens(theme)` in the outer function; +2. use `applyTokenLayers`, `applyPropsToTokens`, or `patchTokens` when state or props must alter resolved tokens; +3. build all slots with `useSlot`; +4. return a hook-free continuation and merge final props there. + +Follow existing examples rather than calling `stagedComponent` directly. + +## Tokens, styling, and state precedence + +- Define defaults as stable objects or theme functions, then place the component name last, for example + `tokens: [defaultButtonTokens, defaultButtonFontTokens, buttonName]`. The name loads `theme.components[name]` + overrides. +- Model visual states as nested values of the same token type. List them in `states` in application order; later + active layers win. +- Supply a lookup to `useSlots` when state is derived from interaction hooks, enum props, context, content, or platform + defaults. Button's `buttonLookup` and Avatar's `avatarLookup` are representative. +- `tokensThatAreAlsoProps` is opt-in. After state layers are applied, listed props override those token values. Prefer + a precise key list over `'all'` to retain caching and avoid leaking unrelated props into tokens. +- Build each slot's cached props with `buildProps`. List every token key read by the builder, including helper key + arrays such as `fontStyles.keys`, `borderStyles.keys`, and `layoutStyles.keys`. +- Treat resolved tokens and cached slot props as immutable. Use `patchTokens` rather than mutation for + instance-dependent values in compressible components. +- Keep theme, default, and state values in token files; token-to-native mapping in styling files; and volatile handlers, + accessibility, data, and ref props in the component state hook or render preparation. +- Platform token differences follow the [design token guidance](../design/tokens-and-theming.md). + +## Hooks, state, and refs + +Put controlled and uncontrolled state, context, interaction hooks, callbacks, accessibility normalization, and ref +preparation in `use` or the outer `useRender` or compressible stage. Return a conventional +`{ props, state }` object when the component has derived behavior; `state` is component-owned data used by the token +lookup and final render. + +Convert `componentRef` to the native root `ref` during preparation, commonly with `useViewCommandFocus`, as Button, +Checkbox, and MenuItem do (R-V1-003). + +## Platform-specific behavior + +Use React Native platform resolution and keep the public props, types, and exports consistent across `.android`, +`.ios`, `.macos`, `.mobile`, `.windows`, and `.win32` implementations. Platform variants may replace the root with a +native component and use a one-slot compose implementation; see +[the macOS experimental Checkbox](../../../packages/experimental/Checkbox/src/Checkbox.macos.tsx) and +[NativeAvatar](../../../packages/experimental/Avatar/src/NativeAvatar.tsx). Those variants also need the native overlay +from the [classification guide](classification.md#4-native-obligations). + +Keep platform defaults and token differences in platform token or styling files when practical. Keep imports from +React Native forks inside their platform files (R-REPO-002) and use the `Platform.OS === ('win32' as any)` convention +described in the [TypeScript reference](../repository/typescript.md). + +## Validation + +Add or update the owning component's Jest tests and snapshots for rendering, state layers, `.customize`, `.compose`, +slot replacement, accessibility, and platform behavior affected by a change. Review snapshot trees rather than updating +them blindly. Run the owning package's declared scripts, for example: + +```bash +yarn workspace @fluentui-react-native/button build +yarn workspace @fluentui-react-native/button test +yarn workspace @fluentui-react-native/button lint +``` + +Changes to the framework itself use `@fluentui-react-native/framework`; changes to factory, slot, token, or styling +semantics also require the matching `@fluentui-react-native/composition`, `use-slots`, `use-tokens`, or `use-styling` +tests. Manual and end-to-end coverage uses the [legacy test apps](legacy-test-apps.md). + +## Review checklist + +- Classification is supported by imports and structure, including compressible components that return a + `directComponent` continuation (R-CLASSIFY-001). +- The change maintains an existing component rather than adding a v1 component or migrating frameworks (R-V1-001, + R-V1-002). +- `componentRef` and existing exports are preserved; no `forwardRef` or incidental public `ref` (R-V1-003). +- Hooks run once in the hook stage; continuations, lookups, and `buildProps` callbacks are hook-free (R-V1-004). +- Slot keys, state order, `tokensThatAreAlsoProps`, and `buildProps` key lists agree with the tokens actually read. +- Platform variants share the public contract; native-root variants received the native overlay. +- Snapshot updates were reviewed rather than accepted blindly. diff --git a/references/agents/design/tokens-and-theming.md b/references/agents/design/tokens-and-theming.md new file mode 100644 index 00000000000..87ababe832b --- /dev/null +++ b/references/agents/design/tokens-and-theming.md @@ -0,0 +1,43 @@ +--- +id: design.tokens-and-theming +kind: instruction-reference +owner_paths: + - packages/agentic/design + - packages/theming +--- + +# Tokens, theming, and color utilities + +- **R-DESIGN-001:** Use [flex-token-map.yaml](../../../packages/agentic/design/src/tokens/mappings/flex-token-map.yaml) + as the canonical mapping from generic CSS and Fluent token sources to grouped React Native Flex token paths. When + mapping an agentic component to an existing V1 component, use the Win32, macOS, or Windows implementation as the + compatibility reference; use iOS only when the work explicitly targets iOS. +- **R-DESIGN-002:** Production code must not depend on `@fluentui-react-native/design/testing`. + +## Tokens + +Tokens are design-time values set through the theme or component customization, for example a brand color. v1 tokens +can also be props through `tokensThatAreAlsoProps`; see the [v1 recipe](../components/v1.md). Agentic token use is +covered by [styles and tokens](../components/agentic/styles-and-tokens.md). + +## Theme-specific styles + +Cache `StyleSheet.create` results that depend only on `ThemeState` with a module-scoped getter created by +`themedStyleSheetFactory`. Treat cached sheets as immutable, and apply props, interaction state, and user styles +separately so instances can share them. + +## Color utilities + +Runtime-safe parsing, conversion, compositing, contrast, and interaction generation live behind the opt-in +`@fluentui-react-native/design/color-lib` subpath. Keep its public exports narrow and add APIs only for demonstrated +production needs. + +Color validation policy, semantic pairings, audit helpers, and conformance fixtures belong under +`packages/agentic/design/src/testing/`. Export them from `@fluentui-react-native/design/testing` when generally useful +to tests. Testing-only validation does not need bundle-size scenarios. + +## Theme packages + +Platform themes live in `packages/theming`: `android-theme`, `apple-theme` (iOS and macOS), `win32-theme`, and +`default-theme`, with shared `theme-tokens` and `theme-types`. Components require a `ThemeProvider` from +`@fluentui-react-native/design/theming`. diff --git a/references/agents/evals/README.md b/references/agents/evals/README.md new file mode 100644 index 00000000000..520b0375295 --- /dev/null +++ b/references/agents/evals/README.md @@ -0,0 +1,53 @@ +--- +id: evals.index +kind: index +owner_paths: + - references/agents +--- + +# Agent evaluation cases + +Portable, sanitized case fixtures for evaluating how agents route and follow this repository's instructions. They are +inputs for a future evaluation framework, not instructions; agents performing ordinary work do not need to read them. +No runner or scoring framework is selected yet. + +## Case fixtures + +Each `cases/.json` file contains: + +| Field | Meaning | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `schema_version` | `1` | +| `case_id` | Kebab-case identifier matching the file name | +| `split` | `development` cases may guide instruction changes; `holdout` cases are reserved for acceptance | +| `task_mode` | `implement`, `maintain`, `review`, `plan`, `contract`, `optimize`, `toolchain`, or `security` | +| `prompt` | The sanitized task given to the agent | +| `expected.skills` | Skills the agent should use | +| `expected.classification` | `implementation_model`, `public_compatibility` labels, and `native_platforms` | +| `expected.required_reads` | Instruction or contract files the agent should read | +| `expected.forbidden_actions` | Any of `edit-files`, `run-mutating-commands`, `create-component`, or `migrate-framework` | +| `assertions[]` | `id`, `description`, `rule_ids`, and `source_refs`; each assertion cites at least one rule ID or source path | + +`implementation_model` is one of `v0`, `v1-compose`, `v1-compressible`, `agentic-higher-order`, `agentic-primitive`, +`framework-base`, or `none`. `native_platforms` lists any of `ios`, `android`, `macos`, `windows`, and `win32`. +`source_refs` are repository paths without line numbers, so ordinary edits do not invalidate them. + +Do not tune instructions against a holdout case. Once a holdout has informed a change, re-label it `development` and +add a new holdout. + +## Execution results + +Execution evidence stays local and is not committed. A result record should contain: + +- `case_id`, the repository commit, and SHA-256 hashes of the instruction files in scope; +- client, client version, model, reasoning setting, and available tools; +- files actually read, skills invoked, and commands run with outcomes; +- per-assertion `pass`, `fail`, or `not-run`, with sanitized evidence; +- token and request usage where the client reports it, and elapsed time; +- known confounders, such as personal instructions, caching, or tool restrictions. + +Do not record raw transcripts, credentials, private source bodies, or personal identifiers. Efficiency measurements are +reported, not used as pass/fail gates. + +`yarn check-agent-instructions` validates the fixtures: schema, unique IDs, existing skills and paths, and defined rule +IDs. diff --git a/references/agents/evals/cases/agentic-button-appearance-plan.json b/references/agents/evals/cases/agentic-button-appearance-plan.json new file mode 100644 index 00000000000..e4a6f3f743c --- /dev/null +++ b/references/agents/evals/cases/agentic-button-appearance-plan.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "agentic-button-appearance-plan", + "split": "development", + "task_mode": "plan", + "prompt": "Plan adding a new 'tinted' appearance to the agentic Button in packages/agentic/components/src/components/button. Plan only.", + "expected": { + "skills": ["agentic-component-authoring"], + "classification": { + "implementation_model": "agentic-higher-order", + "public_compatibility": ["ref-prop"], + "native_platforms": [] + }, + "required_reads": [ + "packages/agentic/components/AGENTS.md", + "packages/agentic/components/src/components/AGENTS.md", + "packages/agentic/components/src/components/button/SPEC.md", + "references/agents/components/agentic/types-and-slots.md", + "references/agents/components/agentic/styles-and-tokens.md" + ], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "contract-before-code", + "description": "Plans a contract revision and review before implementation because the public appearance set changes.", + "rule_ids": ["R-AGENTIC-001"], + "source_refs": ["packages/agentic/components/src/components/button/SPEC.md"] + }, + { + "id": "token-source", + "description": "Maps the new appearance to Flex tokens and documents any token gap.", + "rule_ids": ["R-DESIGN-001"], + "source_refs": ["packages/agentic/design/src/tokens/mappings/flex-token-map.yaml"] + } + ] +} diff --git a/references/agents/evals/cases/apple-extern-header-review.json b/references/agents/evals/cases/apple-extern-header-review.json new file mode 100644 index 00000000000..8c2ce62c997 --- /dev/null +++ b/references/agents/evals/cases/apple-extern-header-review.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "case_id": "apple-extern-header-review", + "split": "development", + "task_mode": "review", + "prompt": "Review a proposed change that adds #import \"FRNNativeCore-Swift.h\" to packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m so a new Paper property bridge can use the Swift CalloutView type.", + "expected": { + "skills": ["review-component", "implement-native-component"], + "classification": { + "implementation_model": "none", + "public_compatibility": ["native-registration"], + "native_platforms": ["macos"] + }, + "required_reads": ["references/agents/native/apple-office.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "flags-generated-header", + "description": "Flags importing the generated Swift header into an RCT_EXTERN_MODULE registration file.", + "rule_ids": ["R-APPLE-003"], + "source_refs": ["packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m"] + }, + { + "id": "bridge-without-swift-types", + "description": "Recommends a Paper property bridge using React or AppKit base types or a Swift-implemented setter instead of generated Swift types.", + "rule_ids": ["R-APPLE-004"], + "source_refs": [] + }, + { + "id": "both-integration-modes", + "description": "Requires module and bridging-header validation, reporting any unrun Office check.", + "rule_ids": ["R-APPLE-007"], + "source_refs": [] + }, + { + "id": "review-is-read-only", + "description": "Reports findings without editing files.", + "rule_ids": ["R-REVIEW-001"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/avatar-swift-react-import-review.json b/references/agents/evals/cases/avatar-swift-react-import-review.json new file mode 100644 index 00000000000..94353b7eece --- /dev/null +++ b/references/agents/evals/cases/avatar-swift-react-import-review.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "avatar-swift-react-import-review", + "split": "holdout", + "task_mode": "review", + "prompt": "Review a proposed change that replaces the guarded React import at the top of packages/experimental/Avatar/ios/AvatarViewManager.swift with a plain `import React`.", + "expected": { + "skills": ["review-component", "implement-native-component"], + "classification": { + "implementation_model": "none", + "public_compatibility": ["native-registration"], + "native_platforms": ["ios"] + }, + "required_reads": ["references/agents/native/apple-office.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "flags-unguarded-import", + "description": "Flags the unguarded Swift React import and keeps the #if canImport(React) guard required for iOS bridging-header builds.", + "rule_ids": ["R-APPLE-002"], + "source_refs": ["packages/experimental/Avatar/ios/AvatarViewManager.swift"] + }, + { + "id": "ios-host-validation", + "description": "Plans validation through an iOS host in both module and bridging-header modes rather than desktop Storybook, reporting unrun Office checks.", + "rule_ids": ["R-APPLE-007"], + "source_refs": [] + }, + { + "id": "review-is-read-only", + "description": "Reports findings without editing files.", + "rule_ids": ["R-REVIEW-001"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/icon-primitive-ref-review.json b/references/agents/evals/cases/icon-primitive-ref-review.json new file mode 100644 index 00000000000..d835c207c94 --- /dev/null +++ b/references/agents/evals/cases/icon-primitive-ref-review.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "icon-primitive-ref-review", + "split": "development", + "task_mode": "review", + "prompt": "Review the Icon primitive in packages/agentic/components/src/primitives/icon. Should it expose a public ref prop like the higher-order components do?", + "expected": { + "skills": ["review-component"], + "classification": { + "implementation_model": "agentic-primitive", + "public_compatibility": ["primitives-entrypoint"], + "native_platforms": [] + }, + "required_reads": [ + "packages/agentic/components/src/primitives/AGENTS.md", + "packages/agentic/components/src/primitives/icon/CONTRACT.md" + ], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "ref-exception", + "description": "Concludes Icon omits ref because its renderers lack one stable imperative instance type, as its contract documents.", + "rule_ids": ["R-PRIMITIVE-002"], + "source_refs": [ + "packages/agentic/components/src/primitives/icon/icon.tsx", + "packages/agentic/components/src/primitives/icon/CONTRACT.md" + ] + }, + { + "id": "review-is-read-only", + "description": "Does not edit the primitive during review.", + "rule_ids": ["R-REVIEW-001"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/ios-react-header-helper.json b/references/agents/evals/cases/ios-react-header-helper.json new file mode 100644 index 00000000000..e3571795571 --- /dev/null +++ b/references/agents/evals/cases/ios-react-header-helper.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "ios-react-header-helper", + "split": "development", + "task_mode": "plan", + "prompt": "Add an Objective-C helper to packages/experimental/AppearanceAdditions that reads the React Native RTL setting from RCTI18nUtil, and call it from Swift. Plan the change.", + "expected": { + "skills": ["implement-native-component"], + "classification": { + "implementation_model": "none", + "public_compatibility": [], + "native_platforms": ["ios"] + }, + "required_reads": ["references/agents/native/apple-office.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "qualified-header", + "description": "Imports with exact case and quotes only local headers.", + "rule_ids": ["R-APPLE-001"], + "source_refs": ["packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m"] + }, + { + "id": "guarded-swift-import", + "description": "Guards any Swift React import with #if canImport(React) for iOS.", + "rule_ids": ["R-APPLE-002"], + "source_refs": [] + }, + { + "id": "both-integration-modes", + "description": "Plans module and bridging-header validation and reports unrun Office checks.", + "rule_ids": ["R-APPLE-007"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/macos-paper-manager-command.json b/references/agents/evals/cases/macos-paper-manager-command.json new file mode 100644 index 00000000000..6905353bd46 --- /dev/null +++ b/references/agents/evals/cases/macos-paper-manager-command.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "case_id": "macos-paper-manager-command", + "split": "development", + "task_mode": "plan", + "prompt": "Add a new imperative command to the macOS Paper Callout view manager in packages/agentic/native-core. Plan the change.", + "expected": { + "skills": ["implement-native-component"], + "classification": { + "implementation_model": "none", + "public_compatibility": ["native-registration"], + "native_platforms": ["macos"] + }, + "required_reads": [ + "references/agents/native/components-and-modules.md", + "references/agents/native/apple-office.md", + "packages/agentic/native-core/README.md" + ], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "swift-manager-command", + "description": "Implements the Paper manager method in Swift and declares it with RCT_EXTERN_METHOD rather than importing the generated Swift header into the registration file.", + "rule_ids": ["R-APPLE-003"], + "source_refs": [ + "packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift", + "packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m" + ] + }, + { + "id": "paper-and-fabric-evidence", + "description": "Plans macOS Paper validation with the --paper Storybook flag, because macOS commands default to Fabric, and reports untested renderers explicitly.", + "rule_ids": ["R-NATIVE-003", "R-APPLE-007"], + "source_refs": ["apps/storybook/AGENTS.md"] + } + ] +} diff --git a/references/agents/evals/cases/native-checkbox-macos-review.json b/references/agents/evals/cases/native-checkbox-macos-review.json new file mode 100644 index 00000000000..e2ffbfd885a --- /dev/null +++ b/references/agents/evals/cases/native-checkbox-macos-review.json @@ -0,0 +1,35 @@ +{ + "schema_version": 1, + "case_id": "native-checkbox-macos-review", + "split": "development", + "task_mode": "review", + "prompt": "Review the macOS experimental Checkbox (packages/experimental/Checkbox/src/Checkbox.macos.tsx) and its native backing. What validation evidence would be required to approve a change to it?", + "expected": { + "skills": ["review-component", "implement-native-component"], + "classification": { + "implementation_model": "v1-compose", + "public_compatibility": ["native-registration"], + "native_platforms": ["macos"] + }, + "required_reads": [ + "references/agents/components/classification.md", + "references/agents/components/v1.md", + "references/agents/native/components-and-modules.md" + ], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "separates-native-obligations", + "description": "Records the v1 wrapper and its macOS native obligations separately.", + "rule_ids": ["R-CLASSIFY-002"], + "source_refs": ["packages/experimental/Checkbox/src/Checkbox.macos.tsx"] + }, + { + "id": "native-evidence", + "description": "Requires a macOS native build and runtime evidence, and does not treat JavaScript tests as native qualification.", + "rule_ids": ["R-NATIVE-003", "R-VALID-002"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/native-core-legacy-callout-prop.json b/references/agents/evals/cases/native-core-legacy-callout-prop.json new file mode 100644 index 00000000000..44d49a1f955 --- /dev/null +++ b/references/agents/evals/cases/native-core-legacy-callout-prop.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "native-core-legacy-callout-prop", + "split": "development", + "task_mode": "plan", + "prompt": "Add a new optional prop to the legacy Callout exported from @fluentui-react-native/native-core/legacy. Plan the change.", + "expected": { + "skills": ["implement-native-component"], + "classification": { + "implementation_model": "framework-base", + "public_compatibility": ["legacy-entrypoint", "componentRef", "native-registration"], + "native_platforms": ["macos", "windows", "win32"] + }, + "required_reads": ["references/agents/native/components-and-modules.md", "packages/agentic/native-core/README.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "legacy-api-modern-implementation", + "description": "Distinguishes the legacy public API from the modern framework-base implementation.", + "rule_ids": ["R-CLASSIFY-002"], + "source_refs": ["packages/agentic/native-core/src/legacy/callout/Callout.tsx"] + }, + { + "id": "keeps-registration", + "description": "Keeps the public API compatible and the native registration names unchanged across hosts.", + "rule_ids": ["R-NATIVE-002"], + "source_refs": ["packages/agentic/native-core/README.md"] + }, + { + "id": "per-host-evidence", + "description": "Plans per-host native validation for macOS, Windows, and Win32.", + "rule_ids": ["R-NATIVE-003"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/native-core-legacy-focuszone-fix.json b/references/agents/evals/cases/native-core-legacy-focuszone-fix.json new file mode 100644 index 00000000000..f821d0e6508 --- /dev/null +++ b/references/agents/evals/cases/native-core-legacy-focuszone-fix.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "case_id": "native-core-legacy-focuszone-fix", + "split": "holdout", + "task_mode": "plan", + "prompt": "The legacy FocusZone exported from @fluentui-react-native/native-core/legacy does not restore focus to its default tabbable element on macOS. Plan the fix.", + "expected": { + "skills": ["implement-native-component"], + "classification": { + "implementation_model": "framework-base", + "public_compatibility": ["legacy-entrypoint", "componentRef", "native-registration"], + "native_platforms": ["macos"] + }, + "required_reads": [ + "references/agents/native/components-and-modules.md", + "references/agents/focus/common.md", + "references/agents/focus/macos.md" + ], + "forbidden_actions": ["edit-files", "migrate-framework"] + }, + "assertions": [ + { + "id": "routes-to-native-work", + "description": "Treats the legacy entrypoint as a framework-base wrapper over native code rather than v0 or v1 maintenance.", + "rule_ids": ["R-CLASSIFY-002"], + "source_refs": ["packages/agentic/native-core/src/legacy/focus-zone/FocusZone.tsx"] + }, + { + "id": "keeps-public-api", + "description": "Keeps the legacy public API, componentRef behavior, and native registration unchanged.", + "rule_ids": ["R-NATIVE-002"], + "source_refs": [] + }, + { + "id": "macos-renderer-evidence", + "description": "Plans macOS Fabric and Paper evidence, using the --paper Storybook flag for Paper, and confirms actual native focus rather than a request result.", + "rule_ids": ["R-NATIVE-003"], + "source_refs": ["packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m"] + } + ] +} diff --git a/references/agents/evals/cases/storybook-macos-bundle-failure.json b/references/agents/evals/cases/storybook-macos-bundle-failure.json new file mode 100644 index 00000000000..4edb0f06650 --- /dev/null +++ b/references/agents/evals/cases/storybook-macos-bundle-failure.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "case_id": "storybook-macos-bundle-failure", + "split": "holdout", + "task_mode": "toolchain", + "prompt": "The agentic Storybook macOS JavaScript bundle started failing after a dependency bump. Plan the diagnosis.", + "expected": { + "skills": ["agentic-storybook-development"], + "classification": { + "implementation_model": "none", + "public_compatibility": [], + "native_platforms": ["macos"] + }, + "required_reads": ["apps/storybook/agent-map.yaml", "apps/storybook/AGENTS.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "declared-scripts", + "description": "Diagnoses with the app-declared Storybook bundle command rather than invoking Metro directly.", + "rule_ids": ["R-VALID-001"], + "source_refs": ["apps/storybook/package.json"] + } + ] +} diff --git a/references/agents/evals/cases/transitive-advisory-remediation.json b/references/agents/evals/cases/transitive-advisory-remediation.json new file mode 100644 index 00000000000..4f3fc0168db --- /dev/null +++ b/references/agents/evals/cases/transitive-advisory-remediation.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "case_id": "transitive-advisory-remediation", + "split": "holdout", + "task_mode": "security", + "prompt": "Dependabot reports a high-severity advisory in a transitive npm dependency. Plan the remediation.", + "expected": { + "skills": ["fix-security-issues"], + "classification": { + "implementation_model": "none", + "public_compatibility": [], + "native_platforms": [] + }, + "required_reads": [".github/skills/fix-security-issues/SKILL.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "keeps-age-gate", + "description": "Keeps the package age gate and selects only eligible fixed versions.", + "rule_ids": ["R-SEC-001"], + "source_refs": [".yarnrc.yml"] + }, + { + "id": "no-suppression", + "description": "Does not substitute an audit exclusion or warning suppression for a fix.", + "rule_ids": ["R-SEC-002"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/v0-button-settings-fix.json b/references/agents/evals/cases/v0-button-settings-fix.json new file mode 100644 index 00000000000..2d60e2eec9e --- /dev/null +++ b/references/agents/evals/cases/v0-button-settings-fix.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "v0-button-settings-fix", + "split": "development", + "task_mode": "maintain", + "prompt": "The deprecated (v0) Button in packages/components/Button/src/deprecated/Button.tsx does not apply its pressed-state border color override from settings. Plan the fix.", + "expected": { + "skills": ["maintain-v0-v1-component"], + "classification": { + "implementation_model": "v0", + "public_compatibility": ["legacy-export", "componentRef"], + "native_platforms": [] + }, + "required_reads": ["references/agents/components/classification.md", "references/agents/components/v0.md"], + "forbidden_actions": ["migrate-framework", "create-component"] + }, + "assertions": [ + { + "id": "classifies-v0-by-imports", + "description": "Identifies v0 from foundation-compose imports, not the deprecated directory alone.", + "rule_ids": ["R-CLASSIFY-001"], + "source_refs": ["packages/components/Button/src/deprecated/Button.tsx"] + }, + { + "id": "fixes-through-settings-layers", + "description": "Fixes the pressed override through settings layering and _overrides without bypassing mergeSettings.", + "rule_ids": ["R-V0-002"], + "source_refs": ["packages/components/Button/src/deprecated/Button.settings.ts"] + }, + { + "id": "no-framework-migration", + "description": "Keeps the fix in v0 rather than migrating to v1 or framework-base.", + "rule_ids": ["R-V0-001"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/v1-button-review.json b/references/agents/evals/cases/v1-button-review.json new file mode 100644 index 00000000000..c52ffbe4353 --- /dev/null +++ b/references/agents/evals/cases/v1-button-review.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "v1-button-review", + "split": "development", + "task_mode": "review", + "prompt": "Review the v1 Button in packages/components/Button/src/Button.tsx for correct ref handling and token state layering. This is a review; report findings.", + "expected": { + "skills": ["review-component"], + "classification": { + "implementation_model": "v1-compose", + "public_compatibility": ["v1-export", "componentRef"], + "native_platforms": [] + }, + "required_reads": ["references/agents/components/classification.md", "references/agents/components/v1.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "review-is-read-only", + "description": "Produces findings without editing files or updating snapshots.", + "rule_ids": ["R-REVIEW-001"], + "source_refs": [] + }, + { + "id": "checks-componentref", + "description": "Checks that componentRef is converted to the native root ref and no forwardRef or public ref was introduced.", + "rule_ids": ["R-V1-003"], + "source_refs": ["packages/components/Button/src/Button.tsx", "packages/components/Button/src/useButton.ts"] + }, + { + "id": "cites-evidence", + "description": "Each finding cites path and line plus a rule or requirement ID, labeled verified or inferred.", + "rule_ids": ["R-REVIEW-002"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/v1-text-compressible-review.json b/references/agents/evals/cases/v1-text-compressible-review.json new file mode 100644 index 00000000000..f06d5a925da --- /dev/null +++ b/references/agents/evals/cases/v1-text-compressible-review.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "case_id": "v1-text-compressible-review", + "split": "development", + "task_mode": "review", + "prompt": "A contributor proposes converting packages/components/Text/src/Text.tsx to framework-base phasedComponent because it already returns a directComponent continuation. Review that proposal.", + "expected": { + "skills": ["review-component"], + "classification": { + "implementation_model": "v1-compressible", + "public_compatibility": ["v1-export", "componentRef"], + "native_platforms": [] + }, + "required_reads": ["references/agents/components/classification.md", "references/agents/components/v1.md"], + "forbidden_actions": ["edit-files", "migrate-framework"] + }, + "assertions": [ + { + "id": "helper-does-not-reclassify", + "description": "Treats the directComponent continuation as part of a v1 compressible component.", + "rule_ids": ["R-CLASSIFY-001"], + "source_refs": ["packages/components/Text/src/Text.tsx"] + }, + { + "id": "rejects-incidental-migration", + "description": "Flags the phasedComponent conversion as a framework migration outside maintenance scope.", + "rule_ids": ["R-V1-002"], + "source_refs": [] + } + ] +} diff --git a/references/agents/evals/cases/windows-fabric-callout-prop.json b/references/agents/evals/cases/windows-fabric-callout-prop.json new file mode 100644 index 00000000000..b64ad1d624c --- /dev/null +++ b/references/agents/evals/cases/windows-fabric-callout-prop.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "case_id": "windows-fabric-callout-prop", + "split": "holdout", + "task_mode": "plan", + "prompt": "Add a new property to native-core's React Native Windows Fabric Callout view. Plan the change.", + "expected": { + "skills": ["implement-native-component"], + "classification": { + "implementation_model": "none", + "public_compatibility": ["native-registration"], + "native_platforms": ["windows"] + }, + "required_reads": ["references/agents/native/components-and-modules.md", "references/agents/native/windows-fabric.md"], + "forbidden_actions": ["edit-files"] + }, + "assertions": [ + { + "id": "regenerates-codegen", + "description": "Updates the TypeScript specification and regenerates Windows codegen with the declared command instead of editing generated files.", + "rule_ids": [], + "source_refs": ["references/agents/native/windows-fabric.md", "packages/agentic/native-core/package.json"] + }, + { + "id": "keeps-registration", + "description": "Keeps the Callout registration name and public API unchanged.", + "rule_ids": ["R-NATIVE-002"], + "source_refs": [] + }, + { + "id": "windows-evidence", + "description": "Plans a Windows native build and runtime evidence and reports Win32 and macOS as not affected or not run.", + "rule_ids": ["R-NATIVE-003"], + "source_refs": [] + } + ] +} diff --git a/.github/skills/agentic-component-authoring/references/focus.md b/references/agents/focus/common.md similarity index 96% rename from .github/skills/agentic-component-authoring/references/focus.md rename to references/agents/focus/common.md index da21bd550d1..c194ca73d31 100644 --- a/.github/skills/agentic-component-authoring/references/focus.md +++ b/references/agents/focus/common.md @@ -1,13 +1,21 @@ +--- +id: focus.common +kind: instruction-reference +owner_paths: + - packages/agentic/components + - packages/framework-base +--- + # Common desktop focus authoring Read this file for every focusable component, collection, popup, focus hook, or native focus adapter. Then load only the applicable platform detail: -- [Windows and Win32](focus-windows.md) -- [macOS](focus-macos.md) +- [Windows and Win32](windows-win32.md) +- [macOS](macos.md) These files define authoring and review obligations. Execution status in -the [desktop focus plan](../../../../packages/agentic/components/WIN32-FOCUS-PLAN.md) +the [desktop focus plan](../../../packages/agentic/components/WIN32-FOCUS-PLAN.md) distinguishes implemented APIs from native-only follow-ups. Keep each reviewed contract and source-specific divergences authoritative. @@ -182,5 +190,5 @@ or production dependencies on the desktop automation driver. - [React 19 refs and cleanup](https://react.dev/blog/2024/12/05/react-19) - [React imperative handles](https://react.dev/reference/react/useImperativeHandle) - [React Native 0.81 native commands](https://reactnative.dev/docs/0.81/the-new-architecture/fabric-component-native-commands) -- [Types and slots](types-and-slots.md) -- [State and accessibility](state-and-accessibility.md) +- [Types and slots](../components/agentic/types-and-slots.md) +- [State and accessibility](../components/agentic/state-and-accessibility.md) diff --git a/.github/skills/agentic-component-authoring/references/focus-macos.md b/references/agents/focus/macos.md similarity index 65% rename from .github/skills/agentic-component-authoring/references/focus-macos.md rename to references/agents/focus/macos.md index 990010bb1af..7190d0dc320 100644 --- a/.github/skills/agentic-component-authoring/references/focus-macos.md +++ b/references/agents/focus/macos.md @@ -1,14 +1,21 @@ +--- +id: focus.macos +kind: instruction-reference +owner_paths: + - packages/agentic/components + - packages/framework-base +--- + # macOS focus authoring -Read [common focus authoring](focus.md) first. Match shared accessibility goals, +Read [common focus authoring](common.md) first. Match shared accessibility goals, but do not transplant Windows click-focus, modifier, or key-up policies into AppKit. -**Reviewed implementation:** local `react-native-macos` 0.81.9, including Fabric -View, repository FocusZone, and Callout implementations, September 14, 2026. -Verify the renderer branch and resolved component before use. A `.macos.tsx` -file can be a stub: V1 Checkbox's macOS entrypoint warns and renders null, so it -is not a valid macOS focus reference. +Version-bound observations behind these rules are in the +[platform evidence](platform-evidence.md), C-FOCUS-001 to C-FOCUS-007. Verify the +renderer branch and resolved component before use. A `.macos.tsx` file can be a +stub that is not a valid macOS focus reference (C-FOCUS-001). ## First responder, key window, and actual target @@ -30,18 +37,13 @@ focus visual should be presented as active. detach/recycle and use common React 19 ref composition, not legacy componentRef exposure or global numeric-tag lookup. -Local references: -`packages\agentic\native-core\macos\components\FocusZone\shared\RCTFocusZone.m:24-35,122-129`; -`packages\agentic\native-core\macos\components\FocusZone\fabric\RCTFocusZoneComponentView.mm:118-179`. -Installed RNmacOS `React\Fabric\Mounting\ComponentViews\View\RCTViewComponentView.mm:1719-1804` -implements focus/blur commands and responder event emission. +Evidence: C-FOCUS-002. ## Pointer focus and click-through -Do not force Windows-style focus-before-press on ordinary macOS controls. -V1 `useOnPressWithFocus` recognizes macOS as focus-capable but only requests -click focus on Windows/Win32. The installed Fabric View also overrides -`needsPanelToBecomeKey` with a comment preserving mouse-versus-keyboard focus. +Do not force Windows-style focus-before-press on ordinary macOS controls. V1 +requests click focus only on Windows/Win32, and the Fabric View preserves +mouse-versus-keyboard focus (C-FOCUS-003). `acceptsFirstMouse` controls click-through in an inactive window; it is not an alias for `focusable`, first-responder acceptance, or keyboard ring visibility. @@ -49,16 +51,13 @@ Test first click to activate a window separately from an actual control action. Menus can intentionally focus on hover, so document that owner-specific exception rather than forcing all click/hover paths into one rule. -Evidence: `packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29`; -installed RNmacOS `RCTViewComponentView.mm:1769-1779`; -`packages\components\Menu\src\MenuItem\useMenuItem.ts:68-120`. +Evidence: C-FOCUS-003. ## Keyboard navigation, shortcuts, and editing -- V1 key helpers prefer key-down on macOS, versus key-up on Windows/Win32. - The macOS branches use `validKeysDown`/`validKeysUp`; current RNmacOS native - handling also has `keyDownEvents`/`keyUpEvents`. Resolve and validate the - compatibility path instead of configuring both indiscriminately. +- V1 and RNmacOS expose both key-down and key-up handling paths, and V1 prefers + key-down on macOS (C-FOCUS-004). Resolve and validate the compatibility path + instead of configuring both indiscriminately. - Matching native handled-key entries suppress AppKit's `super` handling. Register only keys owned by the control/scope so key equivalents, native navigation, and text editing are not swallowed. @@ -67,8 +66,8 @@ installed RNmacOS `RCTViewComponentView.mm:1769-1779`; activation. Do not copy Win32's modifier allowance or TabList Ctrl+Tab behavior. - Confirm exactly-once keyboard/accessibility activation; an AX action is not equivalent evidence to a physical key sequence. -- RNmacOS Pressability activates Return/Space on keydown; keyup supplies - `onPressOut`. Keep native press cleanup separate from Windows keyup-activation +- RNmacOS Pressability activates on key-down and releases on key-up + (C-FOCUS-004). Keep native press cleanup separate from Windows keyup-activation pairing. Intervening keys, caller cancellation, blur, disable, and target replacement must not strand pressed feedback or duplicate release callbacks. Retain a pooled event only with `persist()` and release that reference when @@ -90,38 +89,34 @@ installed RNmacOS `RCTViewComponentView.mm:1769-1779`; - Do not equate VoiceOver navigation focus with the application's keyboard first responder. Validate role/name/value and native action behavior in both modes. -Evidence: `packages\utils\interactive-hooks\src\useKeyProps.ts:8-95,114-126`; -installed RNmacOS `RCTViewComponentView.mm:1807-1861`. +Evidence: C-FOCUS-004. ## FocusZone and Fabric wrapper lifetime -The local macOS FocusZone translates key events, recalculates the key-view loop, -and falls back to mounted descendant order when AppKit collapses a Fabric-backed -zone to one key-loop entry. The Fabric wrapper deliberately does not participate -as another first-responder stop; it forwards focus to its content view. +The macOS FocusZone owns key translation and key-view-loop recalculation, and its +Fabric wrapper forwards focus rather than becoming another first-responder stop +(C-FOCUS-005). Preserve that single-owner behavior. Do not add a second JS roving loop to the same keys. Recompute eligibility when children mount/unmount and when props change, and clear the default responder on recycle. Retain direction, wrap/stop, RTL, nested zones, scrolling, and editable-child semantics. -Evidence: `packages\agentic\native-core\macos\components\FocusZone\shared\RCTFocusZone.m:584-675`; -`packages\agentic\native-core\macos\components\FocusZone\fabric\RCTFocusZoneComponentView.mm:118-195`. +Evidence: C-FOCUS-005. ## Popup windows and lifecycle -Callout's AppKit window can become key but not main. Its `setInitialFocus` and -`focusWindow` make the popup window key; that alone does not establish a particular -child as first responder. Its separate Fabric touch attachment also demonstrates -why React ancestry is not sufficient evidence for popup input capture. +Making a popup window key does not establish a particular child as first +responder, and React ancestry is not sufficient evidence for popup input capture +(C-FOCUS-006). Define and test initial child focus after content attachment, Escape, nested popup dismissal, outside click, window/application deactivation, and guarded return to the parent control. Do not reactivate the application or steal the outside-click target's focus as an unconditional cleanup action. -The legacy ContextualMenu uses a macOS timer after a layout effect. Treat it as -evidence of a native readiness problem, not a reusable delay constant. Prefer a +Treat the legacy ContextualMenu's post-layout timer (C-FOCUS-006) as evidence of +a native readiness problem, not a reusable delay constant. Prefer a mount/window-ready handshake with cancellation and focus confirmation. Keep a bounded, documented workaround only if the supported renderer has no such path. @@ -130,18 +125,13 @@ the owning popup/window, remove exactly once, and capture weak native owners. Do not install OS-global keyboard hooks or require desktop-driver permissions for ordinary production focus behavior. -Evidence: -`packages\agentic\native-core\macos\components\Callout\shared\CalloutWindow.swift:9-38`; -`packages\agentic\native-core\macos\components\Callout\shared\CalloutView.swift:33-45,142-216`; -`packages\agentic\native-core\macos\components\Callout\fabric\RCTCalloutComponentView.mm:96-132`; -`packages\agentic\native-core\macos\components\Callout\shared\GuardedEventMonitor.swift:1-29`; -`packages\components\ContextualMenu\src\ContextualMenu.tsx:39-55`. +Evidence: C-FOCUS-006. ## Native versus custom ring drawing Keep native macOS rings enabled by default unless the reviewed control requires -a custom path. The installed Fabric View draws a rounded AppKit focus mask only -when `enableFocusRing` is true. A focusable view or an enum property by itself is +a custom path. The Fabric View draws its focus mask only when `enableFocusRing` +is true (C-FOCUS-007). A focusable view or an enum property by itself is not proof that the ring mask/drawing is implemented correctly. When drawing a custom ring, disable the competing native path and preserve the @@ -150,8 +140,7 @@ changes, key-window transitions, clipping, appearance, Increase Contrast, and backing-scale changes. Use AppKit points and conversions; do not copy Windows Composition physical-pixel calculations into NSView layout. -Evidence: installed RNmacOS `RCTViewComponentView.mm:1729-1753` and Apple's -focus-mask documentation below. Ring colors remain a native/theme concern; +Evidence: C-FOCUS-007 and Apple's focus-mask documentation below. Ring colors remain a native/theme concern; do not resolve opaque system colors through a guessed web-color fallback. ## Native service candidates and qualification @@ -166,8 +155,7 @@ use AppKit/Swift/C++ behind that boundary without introducing UIKit assumptions. Run physical keyboard/pointer scenarios, VoiceOver, active/inactive windows, first-click behavior, keyboard-navigation settings, editable field editors, and native/custom rings. Include Paper only where it remains supported and Fabric -as an independently qualified endpoint. This Windows-based documentation -investigation did not execute a macOS native run. +as an independently qualified endpoint. ## Public references diff --git a/references/agents/focus/platform-evidence.md b/references/agents/focus/platform-evidence.md new file mode 100644 index 00000000000..7193dd18130 --- /dev/null +++ b/references/agents/focus/platform-evidence.md @@ -0,0 +1,86 @@ +--- +id: focus.platform-evidence +kind: evidence-reference +owner_paths: + - packages/agentic/components + - packages/agentic/native-core + - packages/framework-base +--- + +# Desktop focus platform evidence + +Version-bound observations behind the [macOS](macos.md) and [Windows and Win32](windows-win32.md) focus rules. +Each claim records what was observed, where, at which version, and its limits. These are evidence, not guarantees: +recheck the lockfile, renderer branch, and resolved component before relying on a claim, and update or retire it when +the pinned dependency changes. Installed-package paths are relative to that package's root. + +## macOS + +- **C-FOCUS-001:** The macOS focus guidance was reviewed against local `react-native-macos` 0.81.9, including the + Fabric View and the repository FocusZone and Callout implementations. Observed: 2026-09-14. Limits: a Windows-based + documentation review with no macOS native run. V1 Checkbox's macOS entrypoint warns and renders null, so it is not a + valid macOS focus reference. +- **C-FOCUS-002:** The installed Fabric View implements focus and blur commands and responder event emission. The + repository FocusZone requests and tracks the first responder natively. Sources: RNmacOS + `React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm:1719-1804`; + `packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m:24-35,122-129`; + `packages/agentic/native-core/macos/components/FocusZone/fabric/RCTFocusZoneComponentView.mm:118-179`. Version: + RNmacOS 0.81.9. Observed: 2026-09-14. +- **C-FOCUS-003:** V1 `useOnPressWithFocus` treats macOS as focus-capable but requests click focus only on Windows and + Win32. The installed Fabric View overrides `needsPanelToBecomeKey` to preserve mouse-versus-keyboard focus. V1 + MenuItem focuses on hover as an owner-specific exception. Sources: + `packages/utils/interactive-hooks/src/useOnPressWithFocus.ts:14-29`; RNmacOS `RCTViewComponentView.mm:1769-1779`; + `packages/components/Menu/src/MenuItem/useMenuItem.ts:68-120`. Version: RNmacOS 0.81.9. Observed: 2026-09-14. +- **C-FOCUS-004:** V1 key helpers prefer key-down on macOS through `validKeysDown` and `validKeysUp`, while RNmacOS + native handling also exposes `keyDownEvents` and `keyUpEvents`. RNmacOS Pressability activates Return and Space on + key-down, and key-up supplies `onPressOut`. Sources: `packages/utils/interactive-hooks/src/useKeyProps.ts:8-95,114-126`; + RNmacOS `RCTViewComponentView.mm:1807-1861`. Version: RNmacOS 0.81.9. Observed: 2026-09-14. +- **C-FOCUS-005:** The macOS FocusZone translates key events, recalculates the key-view loop, and falls back to + mounted descendant order when AppKit collapses a Fabric-backed zone to one key-loop entry. Its Fabric wrapper does + not become another first-responder stop; it forwards focus to its content view. Sources: + `packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m:584-675`; + `packages/agentic/native-core/macos/components/FocusZone/fabric/RCTFocusZoneComponentView.mm:118-195`. Observed: + 2026-09-14. +- **C-FOCUS-006:** Callout's AppKit window can become key but not main. `setInitialFocus` and `focusWindow` make the + popup window key without choosing a child first responder, and Callout has a separate Fabric touch attachment. Its guarded + event monitor provides popup-scoped monitor ownership. The legacy ContextualMenu waits on a macOS timer after a layout + effect. Sources: `packages/agentic/native-core/macos/components/Callout/shared/CalloutWindow.swift:9-38`; + `packages/agentic/native-core/macos/components/Callout/shared/CalloutView.swift:33-45,142-216`; + `packages/agentic/native-core/macos/components/Callout/fabric/RCTCalloutComponentView.mm:96-132`; + `packages/agentic/native-core/macos/components/Callout/shared/GuardedEventMonitor.swift:1-29`; + `packages/components/ContextualMenu/src/ContextualMenu.tsx:39-55`. Observed: 2026-09-14. +- **C-FOCUS-007:** The installed Fabric View draws a rounded AppKit focus mask only when `enableFocusRing` is true. + Source: RNmacOS `RCTViewComponentView.mm:1729-1753`. Version: RNmacOS 0.81.9. Observed: 2026-09-14. + +## Windows and Win32 + +- **C-FOCUS-008:** The reviewed endpoints were `@office-iss/react-native-win32` 0.81.8 (Paper, hosted by the prebuilt + REX host in Storybook) and `react-native-windows` 0.81.35 (Fabric, Windows App SDK Composition). Observed: + 2026-09-14. Limits: these are observed dependency versions, not minimum versions for every feature. +- **C-FOCUS-009:** V1 Windows and Win32 behavior comes from focus-before-press, per-control activation, Win32 tab + selection, and menu hover handling. Sources: `packages/utils/interactive-hooks/src/useOnPressWithFocus.ts:14-29`; + `packages/components/Button/src/useButton.ts:15-17,40-66,99-113`; + `packages/components/TabList/src/Tab/useTab.win32.ts:64-110`; + `packages/components/Menu/src/MenuItem/useMenuItem.ts:68-120`. Observed: 2026-09-14. +- **C-FOCUS-010:** The Fabric FocusZone requests focus with `TryFocus(FocusState::Keyboard)` and then checks the root's + focused component. Source: `packages/agentic/native-core/windows/NativeCore/components/FocusZone/FocusZoneComponentView.cpp:175-224`. + Observed: 2026-09-14. +- **C-FOCUS-011:** RNW's root stores a native focus state, resolves programmatic focus with its own policy, and + returns early when asked to focus the already-focused component. Source: RNW + `Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp:79-118`. Version: RNW 0.81.35. Observed: 2026-09-14. + Limits: a native implementation detail, not a public JavaScript API. +- **C-FOCUS-012:** Native qualification found Office Win32 key events with `code="Unidentified"` and a valid `key`. + The Win32 UIA Toggle and SelectionItem patterns were unavailable without accessibility action declarations, even + though JavaScript state was correct. Version: react-native-win32 0.81.8. Observed: 2026-09-14. +- **C-FOCUS-013:** V1 `useKeyProps` prefers key-up on Windows and Win32 and supplies Win32 handled-key descriptors. Its + Windows branch omits descriptors because of a historical `key` versus `code` mismatch. Sources: + `packages/utils/interactive-hooks/src/useKeyProps.ts:8-95,114-126`; + `packages/agentic/native-core/windows/NativeCore/components/FocusZone/FocusZoneComponentView.cpp:71-133`. + Observed: 2026-09-14. +- **C-FOCUS-014:** V1 uses native Win32 focus rings plus appearance-specific focused borders. RNW 0.81.35 has explicit + focus hosting and native theme handling. Sources: RNW + `Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp` and `Theme.cpp`. Observed: 2026-09-14. +- **C-FOCUS-015:** The Windows and Win32 smoke lanes each covered 18 cases with native assertions. Component focus + cases use the `desktop-focus` tag. A disconnected input desktop left semantic cases passing while every + physical-input case skipped. Win32's enabled-state announcement gap for editors is documented in Input's interaction + companion. Observed: 2026-09-14. Limits: case counts change as the catalog grows. diff --git a/.github/skills/agentic-component-authoring/references/focus-windows.md b/references/agents/focus/windows-win32.md similarity index 61% rename from .github/skills/agentic-component-authoring/references/focus-windows.md rename to references/agents/focus/windows-win32.md index bd677f65cc0..0d8f9a63867 100644 --- a/.github/skills/agentic-component-authoring/references/focus-windows.md +++ b/references/agents/focus/windows-win32.md @@ -1,19 +1,27 @@ +--- +id: focus.windows-win32 +kind: instruction-reference +owner_paths: + - packages/agentic/components + - packages/framework-base +--- + # Windows and Win32 focus authoring -Read [common focus authoring](focus.md) first. This file applies the shipped V1 +Read [common focus authoring](common.md) first. This file applies the shipped V1 Win32 behavioral model to both Windows endpoints, while keeping their native implementations separate. ## Identify the endpoint -| Endpoint | Reviewed local implementation | Native owner | -| --------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- | -| `win32` | `@office-iss/react-native-win32` 0.81.8, Paper; prebuilt REX host in Storybook | Office Win32 host/view implementation | -| `windows` | `react-native-windows` 0.81.35, Fabric | Windows App SDK Composition component views and React Native island | +| Endpoint | Platform package and renderer | Native owner | +| --------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `win32` | `@office-iss/react-native-win32`, Paper; prebuilt REX host in Storybook | Office Win32 host/view implementation | +| `windows` | `react-native-windows`, Fabric | Windows App SDK Composition component views and React Native island | -These are observed dependency versions on September 14, 2026, not minimum-version -guarantees for every feature. Recheck the lockfile, platform resolution, and host -before implementation. Office Win32 Paper is **not** RNW's old UWP XAML renderer, +Reviewed versions and other version-bound observations are in the +[platform evidence](platform-evidence.md), C-FOCUS-008 to C-FOCUS-015. Recheck the +lockfile, platform resolution, and host before implementation. Office Win32 Paper is **not** RNW's old UWP XAML renderer, even though both are described as old-architecture/Paper in some contexts. ## Carry V1 behavior across, not its plumbing @@ -34,11 +42,7 @@ its observed behavior to a shared scenario contract, run V1 on Win32 as the reference, then verify the new implementation independently on both renderers. Do not require matching native element classes or identical ring pixels. -Sources: -`packages\utils\interactive-hooks\src\useOnPressWithFocus.ts:14-29`; -`packages\components\Button\src\useButton.ts:15-17,40-66,99-113`; -`packages\components\TabList\src\Tab\useTab.win32.ts:64-110`; -`packages\components\Menu\src\MenuItem\useMenuItem.ts:68-120`. +Evidence: C-FOCUS-009. ## Focus requests and native identity @@ -51,38 +55,28 @@ Native adapters operate on the correct mounted `ComponentView` in its owning surface/island. They must not reuse Paper UIManager/view-manager identifiers or assume a React ref can be serialized into a TurboModule argument. -The existing Fabric FocusZone uses `TryFocus(FocusState::Keyboard)` and checks -the root's focused component after the request -(`packages\agentic\native-core\windows\NativeCore\components\FocusZone\FocusZoneComponentView.cpp:175-224`). -Reuse this request-versus-observation pattern. Do not report success merely +The existing Fabric FocusZone requests focus and then confirms the root's focused +component (C-FOCUS-010). Reuse this request-versus-observation pattern. Do not report success merely because JavaScript called an optional `.focus()` method. -RNW's reviewed root stores a native focus state and resolves programmatic focus -using its own policy. A request for the already-focused component returns early. -This matters for modality-only changes: native focus reasons and the design +RNW's root resolves programmatic focus with its own policy and ignores a request +for the already-focused component (C-FOCUS-011). This matters for modality-only changes: native focus reasons and the design root's modality are related, but are not automatically the same source. Do not synthesize a blur/refocus cycle simply to redraw a ring. -Installed evidence: -`Microsoft.ReactNative\Fabric\Composition\RootComponentView.cpp:79-118`. -This native implementation detail is not a promised public JavaScript API. - ## Keyboard delivery and exactly-once activation - Inspect the actual installed Pressable and native view handlers. Do not add key-up activation on top of an existing native press action. -- Use `useFocusablePressable` rather than per-control key activation. Native - qualification found Office Win32 events with `code="Unidentified"` and a - valid `key`. The helper supplies one paired key-up fallback only when Win32's +- Use `useFocusablePressable` rather than per-control key activation. Office + Win32 can deliver key events whose `code` is `Unidentified` (C-FOCUS-012). The helper supplies one paired key-up fallback only when Win32's code-based Pressability cannot activate that key; recognized codes stay native. Pairing is cancelled on blur, disable, and target-generation changes. - Preserve required native accessibility actions. Checkbox and Switch supply `Toggle`; - Tab supplies `Select`. The Win32 UIA toggle/selection patterns were unavailable - without those action declarations even though JavaScript state was correct. -- V1 `useKeyProps` prefers key-up on Windows/Win32. It supplies Win32 - handled-key descriptors, but its Windows branch deliberately omits descriptors - because of the historical `key` versus `code` mismatch. Translate and test the - pinned Fabric representation instead of copying either branch blindly. + Tab supplies `Select`. Win32 UIA patterns depend on those declarations + (C-FOCUS-012). +- V1 `useKeyProps` handles Windows and Win32 key descriptors differently + (C-FOCUS-013). Translate and test the pinned Fabric representation instead of copying either branch blindly. - Treat native `Handled`/capture registration and JavaScript `preventDefault`/`stopPropagation` as separate mechanisms. Establish which cancellation reaches native navigation at each phase. @@ -93,14 +87,13 @@ This native implementation detail is not a promised public JavaScript API. and accessibility actions. Global root capture observes input; it must not consume every key. -Sources: `packages\utils\interactive-hooks\src\useKeyProps.ts:8-95,114-126`; -`packages\agentic\native-core\windows\NativeCore\components\FocusZone\FocusZoneComponentView.cpp:71-133`. +Evidence: C-FOCUS-013. ## Native and custom focus visuals Current new-component defaults remain native on Fabric and custom on Win32. -V1 uses native Win32 rings and appearance-specific focused borders, so this is -a choice to qualify, not evidence that Win32 native rings are unavailable. +V1's native Win32 rings (C-FOCUS-014) make this a choice to qualify, not evidence +that Win32 native rings are unavailable. - Request native rings only on the actual focus owner. Menu hover focus and disabled-focusable cases may require a different visibility policy from a @@ -115,9 +108,7 @@ a choice to qualify, not evidence that Win32 native rings are unavailable. lifecycle. React layout units and Composition physical pixels differ; apply the validated point-scale conversion. Test clipping/hosting and recycling. -RNW 0.81.35 has explicit focus hosting and native theme handling in installed -`Microsoft.ReactNative\Fabric\Composition\CompositionViewComponentView.cpp` -and `Theme.cpp`. See [Windows Fabric native components](windows-fabric-native-components.md) +RNW provides explicit focus hosting and native theme handling (C-FOCUS-014). See [Windows Fabric native components](../native/windows-fabric.md) for codegen, UI-thread, registration, native visual, and UIA requirements. ## Collections, popups, and platform services @@ -150,14 +141,13 @@ Gate on actual focused element and action/announcement count, not just selected React state or a mocked ref call. A skipped input test is not qualification. Use owned app/Metro lifecycle commands and preserve unrelated sessions. -The catalog now uses named WDIO callbacks. The complete Windows and Win32 smoke -lanes each cover 18 cases with native assertions; component-focused cases use -the `desktop-focus` tag. Check aggregate result counts for zero skips before -claiming qualification. A disconnected input desktop can leave semantic cases -passing while all physical-input cases skip. +The catalog uses named WDIO callbacks, and component focus cases use the +`desktop-focus` tag (C-FOCUS-015). Check aggregate result counts for zero skips +before claiming qualification; a disconnected input desktop can leave semantic +cases passing while physical-input cases skip. For delegated modifier chords, assert parent delivery and unchanged collection selection, not immovable native focus: the host is allowed to handle the chord. For native editors, distinguish noneditable/nonfocusable from UIA `IsEnabled`; -Win32's observed enabled-state announcement gap is documented in Input's -interaction companion and does not replace native focus assertions. +Win32's enabled-state announcement gap (C-FOCUS-015) does not replace native +focus assertions. diff --git a/references/agents/native/apple-office.md b/references/agents/native/apple-office.md new file mode 100644 index 00000000000..a4a4772442e --- /dev/null +++ b/references/agents/native/apple-office.md @@ -0,0 +1,59 @@ +--- +id: native.apple-office +kind: instruction-reference +owner_paths: + - packages/agentic/native-core + - packages/experimental +--- + +# Apple native authoring and Office integration + +These rules apply to all iOS and macOS native components and modules, including experimental packages. Office may +compile Swift with an Objective-C bridging header instead of CocoaPods' `React` clang module, and may expose React Native +headers only through the `React/` namespace. Preserve both integration modes; do not require a host-specific header-map +workaround. + +- **R-APPLE-001:** Import React Native headers in Objective-C and Objective-C++ as `` with the exact + upstream filename case, for example ``. Use quoted includes for local package headers, not bare + React Native names. +- **R-APPLE-002:** Guard Swift's React import. Existing macOS targets use `#if USE_REACT_AS_MODULE`; iOS targets use + `#if canImport(React)`. When the import is disabled, the host's bridging header must expose the required React Native + declarations using qualified includes. Do not invent substitute React types or silently change the macOS build-flag + contract. +- **R-APPLE-003:** A translation unit containing `RCT_EXTERN_MODULE` must not import its generated `-Swift.h`, + directly or through another header. Bridging-header builds can emit internal Swift manager classes into that generated + header, causing duplicate interfaces against the extern macro. Implement Swift-manager commands in Swift and export + matching Objective-C selectors and argument types with `RCT_EXTERN_METHOD`. +- **R-APPLE-004:** Keep custom Paper property bridges independent of generated Swift types: use React or AppKit base types + and narrowly declared manager helpers, or remapped setters implemented in Swift. Renderer adapters and other + Objective-C or Objective-C++ consumers that do not declare an extern Swift manager may import the generated header when + needed. Keep the interoperability header basename aligned with the pod's `module_name`. +- **R-APPLE-005:** Dispatch bridge-manager view lookup and UI mutation to the main queue; `requiresMainQueueSetup` does + not change a command's method queue. For the existing non-`Sendable` manager handoff, use the narrowly scoped + `nonisolated(unsafe) let manager = self` pattern, with all access inside the main-queue closure. Do not add blanket + `@unchecked Sendable`, disable concurrency checking, or mark shared mutable state unsafe to suppress diagnostics. Verify + the target Swift version and features when introducing concurrency annotations. +- **R-APPLE-006:** Compile cleanly with strict warnings. Mark genuinely unused Objective-C method and block parameters + `__unused`; do not remove required selector arguments or suppress `-Wunused-parameter` or case-mismatch warnings + globally. +- **R-APPLE-007:** Validate changed boundaries in both the CocoaPods module configuration and the bridging-header + configuration without the `React` module. Include an emitted bridging-mode Swift header to verify its declarations, + then syntax-check extern registrations without importing that header. Check strict Swift concurrency and qualified, + case-sensitive React header lookup. Run the owning native build and relevant Paper and Fabric commands when feasible; + an upstream build alone is not Office qualification. Record unavailable Office or runtime checks explicitly rather than + claiming parity (R-NATIVE-003). + +## Examples in this repository + +| Rule | Example | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R-APPLE-002 | macOS: [CalloutView.swift](../../../packages/agentic/native-core/macos/components/Callout/shared/CalloutView.swift); iOS: [AvatarViewManager.swift](../../../packages/experimental/Avatar/ios/AvatarViewManager.swift) | +| R-APPLE-003 | [FRNCalloutManager.m](../../../packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m) declares `RCT_EXTERN_METHOD` commands implemented in [CalloutManager.swift](../../../packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift) | +| R-APPLE-004 | [FRNNativeCore.podspec](../../../packages/agentic/native-core/FRNNativeCore.podspec) sets `module_name`; see the native-core README for the interoperability header | +| R-APPLE-005 | The main-queue manager handoff in [CalloutManager.swift](../../../packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift) | +| R-APPLE-006 | `__unused` block parameters in [RCTFocusZone.m](../../../packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m) | + +PR #4364 is the compatibility precedent for these rules. Native-core's +[macOS layout notes](../../../packages/agentic/native-core/README.md) describe its interoperability header and Paper +manager placement; general native ownership and qualification rules are in +[native components and modules](components-and-modules.md). diff --git a/references/agents/native/components-and-modules.md b/references/agents/native/components-and-modules.md new file mode 100644 index 00000000000..7430b83dd6e --- /dev/null +++ b/references/agents/native/components-and-modules.md @@ -0,0 +1,52 @@ +--- +id: native.components-and-modules +kind: instruction-reference +owner_paths: + - packages/agentic/native-core +--- + +# Native components and modules + +Native code can accompany a v0, v1, or agentic component, or stand alone as a module or service. The +[native-core README](../../../packages/agentic/native-core/README.md) is authoritative for native-core's entrypoints, +JavaScript and specification layout, native architecture, codegen, registration, and delivery plan. + +- **R-NATIVE-001:** Put new modern native components, TurboModules, and native services in + `@fluentui-react-native/native-core` rather than creating another standalone package. Maintain existing native + wrappers in their current packages unless the task explicitly requests a migration. +- **R-NATIVE-002:** Preserve public APIs and native registration names during maintenance and migration. Callout and + FocusZone keep their existing public APIs and registration names after moving to native-core. +- **R-NATIVE-003:** Record native validation per platform and renderer as passed, failed, skipped, not run, or + unsupported, with reasons (R-VALID-002). Native code is qualified only by building and exercising the affected native + boundary; an upstream or JavaScript-only build does not qualify another host such as Office. + +## Native-core boundaries + +- Callout and FocusZone implementations live in native-core. Their former packages are JavaScript-only compatibility + shims under `packages/shim/`. Their JavaScript wrappers live in `src/legacy` and are exported only from + `@fluentui-react-native/native-core/legacy`, not the root entrypoint. +- Export shared APIs from the root only when equivalent implementations exist on every supported desktop host. Scope + platform APIs to `macos`, `windows` (React Native Windows), `win` (Windows and Win32), or `win32`. +- Windows components are Fabric-only. macOS components support Paper and Fabric. +- Native applications must depend on native-core directly for native project discovery and autolinking. + +## Component shape + +Components with native code typically: + +- have one root slot wrapping the native component; +- declare it with `codegenNativeComponent` for New Architecture compatibility; +- may use `constantsToExport` for native-side defaults; +- include a `.podspec` for iOS and macOS. Some wrap FluentUI Apple controls. + +Run `pod install` from the owning app or its `ios`/`macos` directory after dependency changes; see the +[permissions reference](../repository/permissions.md) for approved Pod operations. + +## Platform guidance + +- React Native Windows Fabric views: [Windows Fabric native components](windows-fabric.md). +- Apple native code: follow [Apple native authoring and Office integration](apple-office.md) (R-APPLE-001 through + R-APPLE-007) for every iOS or macOS change, plus the macOS layout notes in the native-core README. +- Focus targets and native focus visuals: [common focus authoring](../focus/common.md) and its platform files. +- Accessibility actions: the [framework-base accessibility instructions](../../../packages/framework-base/src/accessibility/AGENTS.md) + define the transport contract and its native qualification requirements. diff --git a/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md b/references/agents/native/windows-fabric.md similarity index 98% rename from .github/skills/agentic-component-authoring/references/windows-fabric-native-components.md rename to references/agents/native/windows-fabric.md index 7e9ae6f4bef..cb9185678d9 100644 --- a/.github/skills/agentic-component-authoring/references/windows-fabric-native-components.md +++ b/references/agents/native/windows-fabric.md @@ -1,3 +1,10 @@ +--- +id: native.windows-fabric +kind: instruction-reference +owner_paths: + - packages/agentic/native-core +--- + # Windows Fabric native components Use this reference when an agentic component needs a native React Native Windows @@ -6,7 +13,7 @@ against the installed `react-native-windows` package before adopting guidance from a newer RNW branch. For behavior, native target lifetime, and modern React refs, read -[common focus authoring](focus.md) and [Windows/Win32 focus authoring](focus-windows.md). +[common focus authoring](../focus/common.md) and [Windows/Win32 focus authoring](../focus/windows-win32.md). Shipped V1 Win32 behavior is also the model for Fabric, but Office Win32 Paper is not the UWP XAML Paper implementation described in the compatibility table below. Do not transfer its native registration or view-manager assumptions. diff --git a/references/agents/repository/architecture.md b/references/agents/repository/architecture.md new file mode 100644 index 00000000000..00b4140f774 --- /dev/null +++ b/references/agents/repository/architecture.md @@ -0,0 +1,65 @@ +--- +id: repository.architecture +kind: instruction-reference +owner_paths: + - packages + - apps +--- + +# Repository architecture + +FluentUI React Native is a monorepo of React Native components implementing Microsoft's Fluent Design System for +iOS, Android, macOS, Windows, and Win32. It is an alpha-stage library under active development. + +## Layout + +```text +apps/ + bundle-size/ Bundle-size measurement and baselines + component-generator/ Component scaffolding tool + E2E/ Appium/WebdriverIO end-to-end tests for v0/v1 components + fluent-tester/ FluentTester host for Android, iOS, macOS, and Windows + storybook/ Agentic component Storybook for macOS, Windows, and Win32 + tester-core/ Shared FluentTester pages and registry + win32/, win32-81/ Win32 FluentTester hosts +packages/ + agentic/components/ Modern agentic components and public primitives + agentic/design/ Design tokens, theming, styling, and color utilities + agentic/desktop-driver/ Desktop WebDriver test driver + agentic/native-core/ Shared modern native components and TurboModules + agentic/storybook-desktop*/ Reusable desktop Storybook CLI and runtime + components/ Stable v0 and v1 components + experimental/ v0/v1 components under development, including native-backed ones + framework/ v1 framework: composition, framework, use-slots, use-styling, use-tokens, theme + framework-base/ Shared component patterns, slots, hooks, accessibility, and JSX runtime + deprecated/ v0 foundation frameworks (foundation-compose and related packages) + theming/ Platform themes (android, apple, default, win32) and theme types and tokens + shim/ Compatibility entrypoints for moved packages such as Callout and FocusZone + utils/, libraries/, codemods/, dependency-profiles/ +scripts/ fluentui-scripts CLI, shared configs, and repository tasks +docs/ Documentation site source +references/agents/ Shared agent references (this library) +``` + +## Component generations + +The repository contains three component-authoring models plus native code that can accompany any of them: + +- **v0:** `@uifabricshared/foundation-compose` settings-based components; maintenance-only. +- **v1:** `@fluentui-react-native/framework` `compose` and `compressible` components; maintenance-only. Many packages + export both a legacy component and a `V1` component, for example `Button` and `ButtonV1`. +- **Agentic:** `packages/agentic/components`, built with framework-base patterns, reviewed contracts, and design + tokens. New components use this model. +- **Native:** native views, TurboModules, and services. New modern native code belongs in native-core. + +Use the [classification guide](../components/classification.md) to identify the model of the code you are changing. + +## Shared concepts + +- **Slots:** a slot is an inner component in the render tree, such as a Button's `root`, `icon`, and `content`. Slots + enable composition and customization; components wrapping one native component typically have one slot. +- **Tokens:** design-time values set by the theme or by component customization. See + [tokens and theming](../design/tokens-and-theming.md). +- **Platform files:** React Native resolves `.ios`, `.android`, `.macos`, `.windows`, and `.win32` variants. Document + platform differences in the component's `SPEC.md`. +- **Themes:** components require a `ThemeProvider` from `@fluentui-react-native/design/theming`. diff --git a/references/agents/repository/dependency-security.md b/references/agents/repository/dependency-security.md new file mode 100644 index 00000000000..aa59d2ca1a8 --- /dev/null +++ b/references/agents/repository/dependency-security.md @@ -0,0 +1,156 @@ +--- +id: repository.dependency-security +kind: instruction-reference +owner_paths: + - package.json + - .yarnrc.yml +--- + +# Dependency security procedures + +Detailed procedures for the [fix-security-issues skill](../../../.github/skills/fix-security-issues/SKILL.md). The skill owns the workflow, safeguards (R-SEC-001 and R-SEC-002), completion checklist, and failure behavior. + +## Inventory the complete problem + +Start with the full recursive audit rather than a partial summary: + +```bash +yarn npm audit --all --recursive --json --no-deprecations +``` + +Normalize the output by severity, package, installed version, advisory, and +dependent. For every finding: + +1. Read the advisory and identify the first patched version, affected + functions, exploit preconditions, and whether a patch exists. +2. Run `yarn why ` to find every path. A fixed direct path does not + help if a stale compatible transitive path remains. +3. Search repository source and installed dependent source to determine + whether the vulnerable capability is used at runtime, only used by tooling, + or unused. +4. Group duplicate advisories for the same package, but do not omit findings + because one report calls them low priority. + +Treat critical and high findings as required fixes. Address moderate and low +findings whenever a compatible, policy-eligible remediation exists. + +## Enforce the package age policy + +Before resolving versions, inspect the effective Yarn policy: + +```bash +yarn config get npmMinimalAgeGate +yarn config get npmPreapprovedPackages +``` + +Calculate the exact cutoff from the session time. Verify publication timestamps +for proposed versions and, after lockfile generation, every newly selected npm +version in the lockfile diff. + +- Never disable or reduce the age gate to obtain a security fix (R-SEC-001). +- Do not assume `latest` is eligible. +- Check preapproved-package exceptions explicitly; an exception does not prove + that a newly selected release meets the requested age policy. +- Prefer the newest eligible fixed patch within the existing compatible range, + not the newest package release overall. + +## Choose the least risky complete remediation + +Use this order: + +1. **Upgrade the direct dependency.** Prefer a patched version within the + current major and update the shared catalog when it is the source of truth. +2. **Upgrade the transitive parent.** This is preferable when the parent has + already adopted the fixed dependency. +3. **Add a narrow root resolution.** Use the exact vulnerable descriptor when + possible. Review API and module-format compatibility before forcing a new + major, and exercise the dependent package's real code path. +4. **Remove an unused vulnerable capability with `ignore:`.** Use the installed + `@rnx-kit/yarn-plugin-ignore` only after proving the package is not required + by repository workflows. +5. **Carry a local Yarn patch.** Reserve this for required packages with no + eligible published fix. Base it on authoritative upstream work, add a + focused regression check, and document that version-based scanners may + still report the patched version. + +Do not add a broad catch, audit exclusion, or warning suppression in place of a runtime fix (R-SEC-002). + +## Safely use the ignore plugin + +The repository's `ignore:` protocol replaces a package with an empty module. +It is appropriate when a transitive dependency implements an optional +capability that this repository intentionally does not support, such as a +browser downloader in a native-only test path. + +Before adding a resolution such as: + +```json +{ + "resolutions": { + "unused-vulnerable-package": "ignore:" + } +} +``` + +prove all of the following: + +- no repository source imports or invokes the package; +- each dependent imports it lazily or can load with an empty module; +- the unsupported capability fails closed if invoked; +- declared builds, tests, bundles, and CLIs do not require it; +- `yarn why` shows only the understood optional paths. + +After installation, verify the npm package is absent from `yarn.lock`, the +`ignore:` locator is present, dependent modules still load, and an attempted +call cannot silently succeed. Never ignore a package merely to make the audit +output clean. + +## Resolve, audit, and deduplicate iteratively + +After each coherent manifest change: + +```bash +yarn install +yarn npm audit --all --recursive --json --no-deprecations +yarn why +``` + +Yarn may retain an older resolution that still satisfies a transitive range, +even when a fixed version is also installed. Once the remediation set is +complete, run: + +```bash +yarn dedupe --strategy highest +yarn dedupe --check +yarn install --immutable --mode=skip-build +``` + +Re-run the audit after dedupe. If stale vulnerable paths remain, update their +owning range or add a narrow compatible resolution; do not assume another +dedupe pass will update a package with only one descriptor. + +Keep related package families aligned. If repository lint expects an older +vulnerable version, update the shared alignment preset or catalog rather than +scattering exceptions or reverting the security floor. + +## Validate the affected behavior + +Use declared workspace scripts and validate in increasing scope: + +1. Format changed manifests and configuration. +2. Run lockfile and repository structural lint. +3. Run affected workspace lint, build, and tests. +4. Exercise the runtime path affected by every major-version resolution. +5. Bundle each affected Metro/native target when bundler dependencies change. +6. Run the root build and repository test graph when shared manifests, + catalogs, resolutions, or the lockfile change broadly. +7. Add a normal or empty changeset as required by repository policy. +8. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). + +## Example + +If `extract-zip` is present only through optional browser installation code +while the repository runs native Appium automation, inspect both dependents, +confirm their normal modules load without extraction, resolve `extract-zip` to +`ignore:`, regenerate and deduplicate the lockfile, then prove the browser +archive path fails closed and the native tooling still works. diff --git a/references/agents/repository/development.md b/references/agents/repository/development.md new file mode 100644 index 00000000000..5325200357b --- /dev/null +++ b/references/agents/repository/development.md @@ -0,0 +1,100 @@ +--- +id: repository.development +kind: instruction-reference +owner_paths: + - package.json + - lage.config.mjs + - scripts +--- + +# Development commands and validation + +The repository builds as one TypeScript project-references graph compiled by `tsc`. Lage orchestrates bundle, test, +lint, and repository-check tasks. Yarn 4 in pnpm mode manages dependencies (see `.yarnrc.yml`). Commands below match +the declared scripts in the root [package.json](../../../package.json) and [lage.config.mjs](../../../lage.config.mjs). + +## Repository-wide rules + +- **R-VALID-001:** Run commands through the owning workspace's declared scripts. Inspect its `package.json` before + invoking an underlying runner; do not assume scripts such as `jest`, `check`, or `depcheck` exist, and do not bypass + project references with ad-hoc `tsc` file arguments. +- **R-VALID-002:** Report each validation as passed, failed, skipped, not run, or unsupported, with the reason. Never + report an unrun, all-skipped, or unsupported check as passed. JavaScript tests, bundles, and source builds are not + evidence of native runtime behavior, physical input, UIA/AX action dispatch, or Office qualification. +- **R-VALID-003:** Before declaring a code, configuration, or instruction change complete, run `yarn clean-lage-cache` + and then `yarn lage buildci` as final validation. Clearing the Lage cache means incorrect cache invalidation cannot + hide a failure. If that run fails, fix the failure and rerun plain `yarn lage buildci`: tasks that never ran have no + cache entry, so they execute, and tasks that passed after the clear reuse fresh results. Read-only reviews and plans + do not need it. +- **R-REPO-001:** Do not use barrel exports (`export * from '...'`). Use explicit named re-exports such as + `export { Foo, Bar } from './module'` and `export type { Baz } from './module'` so bundlers can tree-shake. +- **R-REPO-004:** Do not copy or mechanically transform private source bodies into this public repository. Record + source identifiers and author original documentation and code. + +## Root commands + +Set up a clone with `yarn` to install dependencies, then `yarn build`. + +| Command | Effect | +| ------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `yarn build` | `lage root-build`: runs `root-prebuild` (package `prebuild` steps), then `tsc -b` over the root graph | +| `yarn build:clean` | Cleans the root TypeScript build, then rebuilds | +| `yarn clean` | `tsc -b --clean` plus `lage clean` | +| `yarn clean-lage-cache` | Clears the Lage task cache (`lage cache --clear`) | +| `yarn lage test` | Package tests after the root build and agentic contract checks | +| `yarn lage lint` | Package lint (oxlint through `fluentui-scripts lint`) | +| `yarn lint-fix` | Package lint with autofix (`FURN_FIX_MODE=true`) | +| `yarn format` / `format:check` | Format or check formatting with oxfmt | +| `yarn lint-repo` | Yarn constraints, dependency alignment, Knip, lockfile lint, and formatting | +| `yarn lint-lockfile` | Lockfile validation | +| `yarn check-publishing` | Published packages must not depend on private packages | +| `yarn check-agent-instructions` | Integrity of agent instructions, skills, references, and evaluation cases | +| `yarn test:agent-permissions` | Tests the agent permission hook | +| `yarn lage test-links` | Markdown link validation, one worker per workspace | +| `yarn bundle:repo` | Package bundles (`lage bundle`) | +| `yarn lage buildci` | CI aggregate: repository lint, publishing check, agent-instruction check, build, test, and lint | +| `yarn change` / `change:check` | Create or validate changesets; see [release](release.md) | + +Lage caches task outputs in `node_modules/.cache/lage`. Iterate with cached runs, and add `--verbose` for detail. +`--no-cache` bypasses the cache for one run but leaves earlier entries in place, so a later cached run can still reuse +results for tasks the uncached run never reached. For final validation, clear the cache instead (R-VALID-003). Before committing moved or renamed files, run +`yarn lage test-links` and update stale links. + +## Package commands + +Each package's `build` script is `tsc -b`, which builds the package with its referenced dependencies. Other tasks run +through the `fluentui-scripts` CLI in [`scripts/`](../../../scripts/package.json). Common package scripts are: + +- `yarn build`: project-references build that emits `lib/`; +- `yarn lint`: oxlint; +- `yarn test`: Jest where the package declares it (`fluentui-scripts jest`); +- `yarn update-snapshots`: Jest snapshot update where declared; +- `yarn format`: formats the package; `yarn format --check` only checks. + +Packages differ. Check the scripts before running them; for example, `apps/storybook` has no `build` or `test` script. +`fluentui-scripts jest` appends every discovered test path after forwarded arguments, so a positional file name does +not isolate one file; use `--testNamePattern` to focus a run. + +## Validation discipline + +- During iteration, run the smallest relevant test, and run package lint or build early after type-heavy edits. +- For final validation, run format, lint, the package build, and package tests, then the root build when public types, + manifests, or project references change. Finish with `yarn clean-lage-cache` and `yarn lage buildci` (R-VALID-003). +- After fixing a failed step, rerun that step and its downstream checks before rerunning the whole chain. +- Prefer committed `*.types.test.ts` coverage for compile-time contracts. Keep exploratory probes outside package + source, or remove them before package validation. +- Run working-directory-sensitive native tools from the owning app workspace. Flags such as CocoaPods' + `--project-directory` select project files but do not change subprocess resolution roots. +- Run `yarn` only after dependency manifests change or when a declared command fails because a dependency is missing. +- Review small, localized diffs directly. Use an independent review agent only after focused validation passes and + when the change still has an ambiguous or cross-cutting contract. +- Keep sessions scoped to one reviewable outcome; start unrelated work in a fresh session. + +## Adding a package + +- Use `workspace:*` for internal dependencies. +- Add the package's `tsconfig.json` to the root `tsconfig.json` `references` so it joins the unified build. +- [Dynamic package extensions](../../../scripts/dynamic.extensions.mts) add TypeScript, Jest, oxlint, and oxfmt + development dependencies when the package has the matching configuration or scripts. +- New components belong in the agentic package and new modern native code in native-core; see the + [classification guide](../components/classification.md). diff --git a/references/agents/repository/permissions.md b/references/agents/repository/permissions.md new file mode 100644 index 00000000000..beb72d0e046 --- /dev/null +++ b/references/agents/repository/permissions.md @@ -0,0 +1,80 @@ +--- +id: repository.permissions +kind: instruction-reference +owner_paths: + - .github/hooks/agent-permissions.json + - .github/scripts/agent-permissions.mjs +--- + +# Agent permissions + +The shared Copilot CLI defaults are implemented by [the repository hook](../../../.github/hooks/agent-permissions.json), +not by prose instructions alone. After trusting this repository, start a new CLI session to load the hook. Node.js must +be on `PATH`; the hook itself needs no dependency installation. + +- **R-PERM-001:** Hook approvals are not a sandbox. Operations the hook does not approve fall through to the CLI's + normal permission handling; they are not blanket-denied, and they are not implicitly authorized. +- **R-PERM-002:** Do not amend commits, discard other people's changes, or rewrite history without explicit + authorization. +- **R-PERM-003:** Never print authentication tokens. Do not commit personal permission files, account names, or + credentials. +- **R-PERM-004:** REST `gh api` calls to user, repository, and search endpoints are approved only for GET requests + without request-body or field flags. GraphQL and API mutations require separate approval. +- **R-PERM-005:** Run Pod operations only from the owning app or its `ios`/`macos` directory. Root-level Pod + invocations, updates of every pod, global cache cleanup, and `pod deintegrate` require separate approval. +- **R-PERM-006:** Process inspection and loopback probes are limited to the constrained forms below. Uploads, redirects, + output files, arbitrary endpoints, and process termination are not pre-approved. +- **R-PERM-007:** Unrestricted `screencapture`, `osascript`, debugger attachment, external crash-report directories, + certificate trust changes, and TCC resets remain separately authorized. Do not grant them broadly to work around a + failed native capability check. +- **R-PERM-008:** Run `yarn test:agent-permissions` after changing the permission policy. + +## Approved operations + +- Read and write files inside this repository. +- Run all Yarn commands, including `yarn` and `yarn install`. +- Inspect Git state, stage changes, create new local commits, list branches, and fetch from `origin` (subject to + R-PERM-002). +- Read GitHub issues, pull requests, workflow definitions, and pipeline runs using `gh` (subject to R-PERM-004). +- Inspect the active account with `gh auth status` and `gh api user --jq .login`, and use `gh auth switch` to select an + existing authenticated account when needed (subject to R-PERM-003). +- Run `pod install` (including `--repo-update` and `--clean-install`) or named `pod update ...` operations, + optionally through `bundle exec`, from the owning app or its `ios`/`macos` directory. A project-directory argument + must resolve to that app's native directory and existing Podfile (R-PERM-005). +- Inspect processes and listeners with constrained `ps` and `lsof` commands in Bash. Probe loopback `/status` and + `/index.json` using `curl -q` (which disables user curl configuration), GET or HEAD, and the instance's reported port + (R-PERM-006). + +## How the hook evaluates commands + +The hook accepts simple Bash or PowerShell commands and `&&` sequences when **every** command is approved. +`cd ./ && ...` is allowed only within the repository; use `./` to avoid Bash `CDPATH`. In Bash, +native-driver variables listed in the hook may prefix Yarn commands, directly for unquoted values or through `env`, for +example `FURN_NATIVE_DRIVER_TEST=1 yarn test` or +`env "FURN_DESKTOP_DRIVER_MACOS_SIGNING_IDENTITY=FURN Development" yarn storybook build-driver --macos`. + +Arbitrary environment overrides, pipelines, redirections, shell expansions, unrecognized options, remote writes, and +destructive Git operations fall through to the CLI's normal permission handling (R-PERM-001). Run commands separately +when practical. File approvals resolve symlinks and are limited to this repository. Yarn scripts and Git hooks can +execute arbitrary code, and explicit approvals intentionally trust them. + +## Native diagnostics and capture + +The Yarn approval covers the supported macOS, Windows, and Win32 Storybook workflows; extra blanket approvals for +Xcode, Swift, PowerShell scripts, Metro, or screen capture are unnecessary. Use the +[Storybook diagnostics commands](../../../apps/storybook/AGENTS.md#native-diagnostics). + +Screenshots require a running driver or app and the operating system's own screen-recording permission; CLI approval +does not grant Accessibility or Screen Recording access. Keep evidence scoped to the registered target and ignored +artifact directories (R-PERM-007). + +## Extending permissions locally + +Contributors can add permissions through the CLI's "don't ask again in this repo" prompt, which saves repository-scoped +approvals in their local `~/.copilot/permissions-config.json`, or through `--allow-tool` flags. Personal hooks can be +defined in the gitignored `.github/copilot/settings.local.json`. To opt out of hooks locally, set +`disableAllHooks: true` there; this disables all non-policy hooks. Hook approvals precede normal tool permission checks, +so disable the hook if CLI deny rules must govern these approved operations. + +See the [Copilot hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference) and the +[saved permissions schema](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference#permissions-configjson). diff --git a/references/agents/repository/release.md b/references/agents/repository/release.md new file mode 100644 index 00000000000..be81513c8ad --- /dev/null +++ b/references/agents/repository/release.md @@ -0,0 +1,19 @@ +--- +id: repository.release +kind: instruction-reference +owner_paths: + - .changeset + - .github/scripts/change.mts +--- + +# Changesets and release + +- **R-REPO-003:** A change to a published package needs a changeset from `yarn change`, or an empty changeset + (`yarn changeset --empty`) when no release is needed. Never bump a private package or introduce a major version. + +`yarn change:check` validates changesets in CI. The check in [change.mts](../../../.github/scripts/change.mts) requires +changesets for changed public packages and rejects private-package and major bumps. Configuration lives in +[`.changeset/config.json`](../../../.changeset/config.json). + +GitHub Actions opens version-bump pull requests automatically; `yarn changeset:version` applies version bumps. +Publishing runs in Azure Pipelines through `.ado/azure-pipelines.publish.yml`. diff --git a/references/agents/repository/typescript.md b/references/agents/repository/typescript.md new file mode 100644 index 00000000000..02564fce482 --- /dev/null +++ b/references/agents/repository/typescript.md @@ -0,0 +1,43 @@ +--- +id: repository.typescript +kind: instruction-reference +owner_paths: + - tsconfig.json + - scripts/configs/tsconfig +--- + +# TypeScript configuration + +The repository uses TypeScript 7.0.2 through `tsc`. [Dynamic package extensions](../../../scripts/dynamic.extensions.mts) +add it to every package that has a `tsconfig.json`. The root `tsconfig.json` lists every package under `references`; +`tsc -b` builds them in dependency order, and each package caches incremental state in `.cache/tsconfig.tsbuildinfo`. + +- **R-REPO-002:** Do not import more than one React Native fork (`react-native`, `react-native-windows`, + `react-native-macos`, `@office-iss/react-native-win32`) into a single program's type graph. Keep fork imports in + `.win32.ts`, `.windows.ts`, or `.macos.ts` files, or redeclare the needed shapes platform-neutrally. + +## Settings + +- Base config: [scripts/configs/tsconfig/tsconfig.json](../../../scripts/configs/tsconfig/tsconfig.json) extends + `tsconfig.strict.json`, which extends `@rnx-kit/tsconfig/tsconfig.nodenext.json`. +- `target` and `lib` are `esnext`; `module` is `esnext` with `bundler` resolution; `jsx` is `react-jsx`. +- `rewriteRelativeImportExtensions` is enabled for ESM relative imports. +- `composite: true` is required for project references, and `outDir` is `lib`. +- Strict mode is relaxed for legacy compatibility: `strictNullChecks`, `noImplicitAny`, and `strictBindCallApply` are + disabled in the base config. + +## Compatibility notes + +- `suppressImplicitAnyIndexErrors` was removed in TypeScript 5.8 and later. +- `Platform.OS` does not include `'win32'` in the React Native types even though react-native-win32 supports it at + runtime. Use `Platform.OS === ('win32' as any)`. +- The React Native forks have overlapping but divergent type definitions. Mixing them produces order-dependent type + confusion under the unified build (R-REPO-002). + +## JSX runtime + +Component files that render callable slots put `/** @jsxImportSource @fluentui-react-native/framework-base */` on the +first line. The custom runtime optimizes slot rendering, so `withSlots` is unnecessary, and it re-exports `Fragment`. +Compiled output imports the runtime, so packages that use it declare `@fluentui-react-native/framework-base` as a +dependency. The pragma alone does not identify a component's generation; see the +[classification guide](../components/classification.md). diff --git a/scripts/package.json b/scripts/package.json index 079796b1a9e..ac91030fd8b 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -29,7 +29,8 @@ "build": "tsc -b", "bundlesize": "bundlesize --debug", "format": "node ./src/cli.ts format", - "lint": "node ./src/cli.ts lint" + "lint": "node ./src/cli.ts lint", + "test": "node --test src/tasks/agentInstructions.test.ts" }, "dependencies": { "@babel/core": "catalog:", diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index b059ff5f0a8..273f6b2de60 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -6,6 +6,7 @@ import { FormatCommand } from './tasks/format.ts'; import { LintCommand } from './tasks/lint.ts'; import { JestCommand } from './tasks/jest.ts'; import { CheckPublishingCommand } from './tasks/checkPublishingTask.ts'; +import { CheckAgentInstructionsCommand } from './tasks/checkAgentInstructionsTask.ts'; const cli = new Cli({ binaryLabel: 'fluentui-scripts CLI', @@ -18,6 +19,7 @@ cli.register(FormatCommand); cli.register(LintCommand); cli.register(JestCommand); cli.register(CheckPublishingCommand); +cli.register(CheckAgentInstructionsCommand); cli.register(Builtins.DefinitionsCommand); cli.register(Builtins.HelpCommand); diff --git a/scripts/src/tasks/agentInstructions.test.ts b/scripts/src/tasks/agentInstructions.test.ts new file mode 100644 index 00000000000..8fabc93ace6 --- /dev/null +++ b/scripts/src/tasks/agentInstructions.test.ts @@ -0,0 +1,236 @@ +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, it } from 'node:test'; + +import { checkAgentInstructions, collectAnchors, extractPrerequisites, parseFrontmatter, slugify } from './agentInstructions.ts'; + +const roots: string[] = []; + +function reference(id: string, kind: string, body: string, owner = 'packages/demo'): string { + return `---\nid: ${id}\nkind: ${kind}\nowner_paths:\n - ${owner}\n---\n\n${body}`; +} + +const ROOT = + '# Root\n\nRead the [index](references/agents/README.md) and follow R-DEMO-001.\n\n| Task | Skill |\n| --- | --- |\n| Demo work | [demo-skill](.github/skills/demo-skill/SKILL.md) |\n'; + +const VALID: Record = { + 'packages/demo/index.ts': 'export {};\n', + 'packages/demo/SPEC.md': '# Demo\n\nSee [policy](AGENTS.md#focus-policy).\n', + 'packages/demo/AGENTS.md': '# Demo\n\n## Focus policy\n\n- **R-DEMO-001:** Keep focus visible.\n', + 'AGENTS.md': ROOT, + 'references/agents/README.md': reference( + 'index', + 'index', + '# Index\n\n| ID | Document |\n| --- | --- |\n| `demo.guide` | [Guide](demo/guide.md) |\n', + 'references/agents', + ), + 'references/agents/demo/guide.md': reference('demo.guide', 'instruction-reference', '# Guide\n\nUse `packages/demo/index.ts`.\n'), + '.github/skills/demo-skill/SKILL.md': '---\nname: demo-skill\ndescription: Demonstrates a skill.\nlicense: MIT\n---\n\n# Demo skill\n', + '.github/skills/demo-skill/references/old.md': reference( + 'compat.demo.old', + 'compatibility-reference', + '# Moved\n\nSee [guide](../../../../references/agents/demo/guide.md).\n', + ), + 'references/agents/evals/cases/demo-case.json': JSON.stringify({ + schema_version: 1, + case_id: 'demo-case', + split: 'development', + task_mode: 'review', + prompt: 'Review the demo.', + expected: { + skills: ['demo-skill'], + classification: { implementation_model: 'none', public_compatibility: [], native_platforms: [] }, + required_reads: ['references/agents/demo/guide.md'], + forbidden_actions: ['edit-files'], + }, + assertions: [ + { id: 'keeps-focus', description: 'Keeps focus visible.', rule_ids: ['R-DEMO-001'], source_refs: ['packages/demo/index.ts'] }, + ], + }), +}; + +function fixture(overrides: Record = {}): string { + const root = mkdtempSync(path.join(tmpdir(), 'agent-instructions-')); + roots.push(root); + for (const [file, content] of Object.entries({ ...VALID, ...overrides })) { + if (content === undefined) { + continue; + } + mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + writeFileSync(path.join(root, file), content); + } + return root; +} + +function messages(overrides: Record): string[] { + return checkAgentInstructions(fixture(overrides)).issues.map((issue) => `${issue.file}: ${issue.message}`); +} + +function assertIssue(overrides: Record, pattern: RegExp) { + const found = messages(overrides); + assert.ok( + found.some((message) => pattern.test(message)), + `expected ${pattern} in:\n${found.join('\n')}`, + ); +} + +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } +}); + +describe('agent instruction helpers', () => { + it('creates GitHub-style anchors', () => { + assert.equal(slugify('4. Native obligations'), '4-native-obligations'); + assert.equal(slugify('`includesGeneratedCode` is not Windows-scoped'), 'includesgeneratedcode-is-not-windows-scoped'); + assert.equal(slugify('Use x and <b>bold'), 'use-x-and-bold'); + assert.deepEqual([...collectAnchors('# A\n## A\n```\n# ignored\n```\n\n')], ['a', 'a-1', 'custom']); + }); + + it('parses only the standardized frontmatter', () => { + assert.deepEqual(parseFrontmatter(reference('a.b', 'index', 'body')), { + id: 'a.b', + kind: 'index', + ownerPaths: ['packages/demo'], + bodyStart: 6, + }); + assert.match(String(parseFrontmatter('# No frontmatter')), /missing frontmatter/); + assert.match(String(parseFrontmatter('---\nid: a\nkind: index\nowner_paths:\n - x\ntitle: extra\n---\n')), /no other keys/); + assert.match(String(parseFrontmatter('---\nkind: index\nid: a\nowner_paths:\n - x\n---\n')), /in that order/); + }); + + it('detects explicit prerequisites', () => { + const text = + '# A\n\nRead [common](common.md) first. Then see [other](other.md).\n\n- Work starts with [x](x.md), then [y](y.md).\n\n## Read first\n\n1. [Package](pkg.md)\n'; + assert.deepEqual( + extractPrerequisites(text).map((link) => link.target), + ['common.md', 'x.md', 'pkg.md'], + ); + }); +}); + +describe('checkAgentInstructions', () => { + it('accepts a valid repository', () => { + assert.deepEqual(messages({}), []); + }); + + it('rejects invalid frontmatter values', () => { + assertIssue({ 'references/agents/demo/guide.md': reference('Demo_Guide', 'instruction-reference', '# G\n') }, /invalid id/); + assertIssue({ 'references/agents/demo/guide.md': reference('demo.guide', 'notes', '# G\n') }, /unknown kind/); + assertIssue( + { 'references/agents/demo/guide.md': reference('demo.guide', 'instruction-reference', '# G\n', 'packages/missing') }, + /owner path/, + ); + assertIssue({ 'references/agents/demo/other.md': reference('demo.guide', 'instruction-reference', '# O\n') }, /duplicate id/); + assertIssue({ '.github/skills/demo-skill/references/plain.md': '# Plain\n' }, /missing frontmatter/); + }); + + it('requires indexed references', () => { + assertIssue({ 'references/agents/demo/new.md': reference('demo.new', 'instruction-reference', '# New\n') }, /not linked from/); + }); + + it('rejects broken links, case mismatches, missing anchors, and missing code paths', () => { + assertIssue({ 'AGENTS.md': `${ROOT}\n[Missing](missing.md)\n` }, /broken link: missing\.md/); + assertIssue({ 'AGENTS.md': `${ROOT}\n[Case](references/agents/README.MD)\n` }, /broken link/); + assertIssue({ 'AGENTS.md': `${ROOT}\n[Anchor](packages/demo/AGENTS.md#absent)\n` }, /missing anchor/); + assertIssue({ 'AGENTS.md': `${ROOT}\nRun \`packages/demo/missing.ts\`.\n` }, /code span does not exist/); + }); + + it('protects inbound anchors into instruction files', () => { + assertIssue( + { 'packages/demo/AGENTS.md': '# Demo\n\n- **R-DEMO-001:** Keep focus visible.\n' }, + /SPEC\.md: missing anchor in instruction file/, + ); + }); + + it('requires rule IDs to be defined exactly once', () => { + assertIssue({ 'AGENTS.md': `${ROOT}\nFollow R-DEMO-002.\n` }, /undefined rule ID R-DEMO-002/); + assertIssue({ 'AGENTS.md': `${ROOT}\n- **R-DEMO-001:** Duplicate.\n` }, /already defined/); + }); + + it('rejects compatibility redirects without a live target', () => { + assertIssue( + { + '.github/skills/demo-skill/references/older.md': reference( + 'compat.demo.older', + 'compatibility-reference', + '# Moved\n\n[Old](old.md)\n', + ), + }, + /only to another compatibility reference/, + ); + }); + + it('validates skill metadata', () => { + assertIssue({ '.github/skills/demo-skill/SKILL.md': '---\nname: other\ndescription: Text.\n---\n' }, /must match its directory/); + assertIssue({ '.github/skills/demo-skill/SKILL.md': `---\nname: demo-skill\ndescription: ${'word '.repeat(61)}\n---\n` }, /61 words/); + }); + + it('rejects prerequisite cycles and deep chains', () => { + const doc = (id: string, body: string) => reference(id, 'instruction-reference', body); + const index = reference( + 'index', + 'index', + '# Index\n\n| ID | Document |\n| --- | --- |\n| `demo.guide` | [Guide](demo/guide.md) |\n| `demo.a` | [A](demo/a.md) |\n| `demo.b` | [B](demo/b.md) |\n| `demo.c` | [C](demo/c.md) |\n| `demo.d` | [D](demo/d.md) |\n', + 'references/agents', + ); + assertIssue( + { + 'references/agents/README.md': index, + 'references/agents/demo/a.md': doc('demo.a', '# A\n\nRead [B](b.md) first.\n'), + 'references/agents/demo/b.md': doc('demo.b', '# B\n\nRead [A](a.md) first.\n'), + 'references/agents/demo/c.md': doc('demo.c', '# C\n'), + 'references/agents/demo/d.md': doc('demo.d', '# D\n'), + }, + /prerequisite cycle: .*a\.md -> .*b\.md -> .*a\.md|prerequisite cycle: .*b\.md -> .*a\.md -> .*b\.md/, + ); + assertIssue( + { + 'references/agents/README.md': index, + 'references/agents/demo/a.md': doc('demo.a', '# A\n\nRead [B](b.md) first.\n'), + 'references/agents/demo/b.md': doc('demo.b', '# B\n\nRead [C](c.md) first.\n'), + 'references/agents/demo/c.md': doc('demo.c', '# C\n\nRead [D](d.md) first.\n'), + 'references/agents/demo/d.md': doc('demo.d', '# D\n\nRead [guide](guide.md) first.\n'), + }, + /prerequisite chain is 4 reads deep/, + ); + }); + + it('keeps compatibility references terminal', () => { + assertIssue( + { 'packages/demo/README.md': '# Demo\n\n[Old](../../.github/skills/demo-skill/references/old.md)\n' }, + /links to compatibility reference/, + ); + }); + + it('requires single, consistent, complete skill routing', () => { + assertIssue( + { 'AGENTS.md': '# Root\n\n| Task | Skill |\n| --- | --- |\n| Demo work | none |\n' }, + /must link exactly one skill; found 0/, + ); + assertIssue({ 'AGENTS.md': '# Root\n' }, /not routed from the root AGENTS\.md routing table/); + assertIssue( + { + '.github/skills/other-skill/SKILL.md': '---\nname: other-skill\ndescription: Another skill.\n---\n', + 'AGENTS.md': `${ROOT}| Other work | [other-skill](.github/skills/other-skill/SKILL.md) |\n`, + 'packages/demo/AGENTS.md': + '# Demo\n\n## Focus policy\n\n- **R-DEMO-001:** Keep focus visible.\n\n| Task | Workflow |\n| --- | --- |\n| Demo work | [other](../../.github/skills/other-skill/SKILL.md) |\n', + }, + /task "demo work" routes to/, + ); + }); + + it('validates evaluation cases', () => { + const base = JSON.parse(VALID['references/agents/evals/cases/demo-case.json']); + const write = (value: object) => ({ 'references/agents/evals/cases/demo-case.json': JSON.stringify(value) }); + assertIssue(write({ ...base, case_id: 'other' }), /must match the file name/); + assertIssue(write({ ...base, expected: { ...base.expected, skills: ['missing-skill'] } }), /unknown skill/); + assertIssue(write({ ...base, expected: { ...base.expected, required_reads: ['missing.md'] } }), /path does not exist/); + assertIssue(write({ ...base, assertions: [{ ...base.assertions[0], rule_ids: ['R-DEMO-404'] }] }), /undefined rule/); + assertIssue(write({ ...base, assertions: [{ id: 'empty', description: 'No evidence.', rule_ids: [], source_refs: [] }] }), /must cite/); + assertIssue(write({ ...base, split: 'training' }), /split must be/); + }); +}); diff --git a/scripts/src/tasks/agentInstructions.ts b/scripts/src/tasks/agentInstructions.ts new file mode 100644 index 00000000000..f2a31987bf9 --- /dev/null +++ b/scripts/src/tasks/agentInstructions.ts @@ -0,0 +1,747 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; + +/** + * Deterministic integrity checks for agent instructions, skills, shared references, and evaluation fixtures. + * The conventions are documented in references/agents/README.md and references/agents/evals/README.md. + */ + +export type Issue = { file: string; line?: number; message: string }; + +export type CheckResult = { + issues: Issue[]; + report: { file: string; bytes: number }[]; +}; + +const REFERENCES_DIR = 'references/agents'; +const EVAL_CASES_DIR = `${REFERENCES_DIR}/evals/cases`; +const SKILLS_DIR = '.github/skills'; +const ALWAYS_ON = ['AGENTS.md', 'CLAUDE.md', '.github/copilot-instructions.md']; + +const KINDS = new Set(['index', 'instruction-reference', 'evidence-reference', 'compatibility-reference']); +const INDEXED_KINDS = new Set(['instruction-reference', 'evidence-reference']); +const ID_PATTERN = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/; +const RULE_DEFINITION = /^\s*[-*]\s+\*\*([RC]-[A-Z0-9]+-\d{3}):\*\*/; +const RULE_CITATION = /\b[RC]-[A-Z0-9]+-\d{3}\b/g; +const MAX_SKILL_DESCRIPTION_WORDS = 60; +const MAX_PREREQUISITE_DEPTH = 3; +const PREREQUISITE_SENTENCE = /\b(?:read|load)\b[^.]*\bfirst\b|\bstarts? with\b/i; +const ROUTING_COLUMNS = new Set(['skill', 'workflow']); +const SKILL_LINK = /(?:^|\/)\.github\/skills\/([^/]+)\/SKILL\.md$/; +const SKIP_DIRS = new Set(['node_modules', 'lib', 'lib-commonjs', 'dist', '.git', 'Pods', 'build', 'DerivedData']); +const REPO_PATH_PREFIXES = ['.github/', 'apps/', 'packages/', 'references/', 'scripts/', 'docs/']; + +const TASK_MODES = new Set(['implement', 'maintain', 'review', 'plan', 'contract', 'optimize', 'toolchain', 'security']); +const SPLITS = new Set(['development', 'holdout']); +const MODELS = new Set(['v0', 'v1-compose', 'v1-compressible', 'agentic-higher-order', 'agentic-primitive', 'framework-base', 'none']); +const PLATFORMS = new Set(['ios', 'android', 'macos', 'windows', 'win32']); +const FORBIDDEN_ACTIONS = new Set(['edit-files', 'run-mutating-commands', 'create-component', 'migrate-framework']); + +type Frontmatter = { id: string; kind: string; ownerPaths: string[]; bodyStart: number }; + +/** Lists repository files, preferring Git so ignored output is excluded. Falls back to a filesystem walk. */ +export function listFiles(root: string): string[] { + try { + const output = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + maxBuffer: 64 * 1024 * 1024, + }); + return output + .split('\0') + .filter(Boolean) + .filter((file) => existsSync(path.join(root, file))); + } catch { + const files: string[] = []; + const walk = (dir: string) => { + for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) { + const rel = dir ? `${dir}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name)) { + walk(rel); + } + } else if (entry.isFile()) { + files.push(rel); + } + } + }; + walk(''); + return files; + } +} + +/** Returns true when a repository-relative path exists with exactly this letter case. */ +export function existsExact(root: string, rel: string): boolean { + const normalized = path.posix.normalize(rel).replace(/\/$/, ''); + if (normalized === '' || normalized === '.') { + return true; + } + if (normalized.startsWith('..')) { + return false; + } + let current = root; + for (const segment of normalized.split('/')) { + let entries: string[]; + try { + entries = readdirSync(current); + } catch { + return false; + } + if (!entries.includes(segment)) { + return false; + } + current = path.join(current, segment); + } + return true; +} + +/** Returns true when Git ignores the path, such as documented personal configuration files. */ +function isGitIgnored(root: string, rel: string): boolean { + try { + execFileSync('git', ['check-ignore', '-q', '--no-index', rel], { cwd: root, stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +/** Removes inline HTML tags from heading text by scanning, so nested or malformed markup cannot survive. */ +function removeTags(text: string): string { + let result = ''; + let depth = 0; + for (const character of text) { + if (character === '<') { + depth++; + } else if (character === '>' && depth > 0) { + depth--; + } else if (depth === 0) { + result += character; + } + } + return result; +} + +/** GitHub-style heading anchor. */ +export function slugify(heading: string): string { + return removeTags(heading.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')) + .trim() + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, '') + .replace(/\s/g, '-'); +} + +/** Collects heading anchors and explicit HTML anchors from Markdown, ignoring fenced code. */ +export function collectAnchors(markdown: string): Set { + const anchors = new Set(); + const counts = new Map(); + let fenced = false; + for (const line of markdown.split('\n')) { + if (/^\s*(```|~~~)/.test(line)) { + fenced = !fenced; + continue; + } + if (fenced) { + continue; + } + const heading = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line); + if (heading) { + const base = slugify(heading[1]); + const count = counts.get(base) ?? 0; + counts.set(base, count + 1); + anchors.add(count === 0 ? base : `${base}-${count}`); + } + for (const match of line.matchAll(/ { + if (/^\s*(```|~~~)/.test(raw)) { + fenced = !fenced; + return; + } + if (fenced) { + return; + } + const line = raw.replace(/`[^`]*`/g, '``'); + for (const match of line.matchAll(/\]\(]+)>?(?:\s+"[^"]*")?\)/g)) { + links.push({ target: match[1], line: index + 1 }); + } + const definition = /^\s*\[[^\]]+\]:\s*(\S+)/.exec(line); + if (definition) { + links.push({ target: definition[1], line: index + 1 }); + } + }); + return links; +} + +/** Extracts inline code spans that look like repository paths. */ +export function extractCodePaths(markdown: string): Link[] { + const paths: Link[] = []; + let fenced = false; + markdown.split('\n').forEach((line, index) => { + if (/^\s*(```|~~~)/.test(line)) { + fenced = !fenced; + return; + } + if (fenced) { + return; + } + for (const match of line.matchAll(/`([^`\s]+)`/g)) { + const value = match[1]; + if (REPO_PATH_PREFIXES.some((prefix) => value.startsWith(prefix)) && !/[<>*{}|$]|\.\.\./.test(value)) { + paths.push({ target: value.replace(/[#:].*$/, ''), line: index + 1 }); + } + } + }); + return paths; +} + +/** + * Extracts explicit prerequisites: the first link in a sentence such as "Read [X](x.md) first" or + * "Focus work starts with [X](x.md)", and every link in a "Read first" section. + */ +export function extractPrerequisites(markdown: string): Link[] { + const prerequisites: Link[] = []; + let fenced = false; + let readFirstSection = false; + let paragraph: { text: string; line: number } | undefined; + const flush = () => { + if (!paragraph) { + return; + } + for (const sentence of paragraph.text.split(/(?<=[.!?])\s+(?=[A-Z[`*])/)) { + if (PREREQUISITE_SENTENCE.test(sentence.replace(/\[[^\]]*\]\([^)]*\)/g, 'LINK'))) { + const [first] = extractLinks(sentence); + if (first) { + prerequisites.push({ target: first.target, line: paragraph.line }); + } + } + } + paragraph = undefined; + }; + markdown.split('\n').forEach((line, index) => { + if (/^\s*(```|~~~)/.test(line)) { + flush(); + fenced = !fenced; + return; + } + if (fenced) { + return; + } + const heading = /^#{1,6}\s+(.*)$/.exec(line); + if (heading) { + flush(); + readFirstSection = /^read first$/i.test(heading[1].trim()); + return; + } + if (readFirstSection) { + for (const link of extractLinks(line)) { + prerequisites.push({ target: link.target, line: index + 1 }); + } + } + if (line.trim() === '' || line.trimStart().startsWith('|')) { + flush(); + return; + } + if (/^\s*(?:[-*]|\d+\.)\s+/.test(line)) { + flush(); + } + paragraph = paragraph ? { text: `${paragraph.text} ${line.trim()}`, line: paragraph.line } : { text: line.trim(), line: index + 1 }; + }); + flush(); + return prerequisites; +} + +type RoutingRow = { line: number; task: string; skills: string[] }; + +/** Extracts rows of Markdown tables whose header has a Skill or Workflow column. */ +export function extractRoutingRows(markdown: string, fromFile: string): RoutingRow[] { + const rows: RoutingRow[] = []; + const lines = markdown.split('\n'); + const cells = (line: string) => + line + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => cell.trim()); + for (let index = 0; index < lines.length; index++) { + if (!lines[index].trimStart().startsWith('|') || !/^\s*\|[\s:|-]+\|\s*$/.test(lines[index + 1] ?? '')) { + continue; + } + const header = cells(lines[index]).map((cell) => cell.toLowerCase()); + const column = header.findIndex((cell) => ROUTING_COLUMNS.has(cell)); + if (column <= 0) { + continue; + } + for (let row = index + 2; row < lines.length && lines[row].trimStart().startsWith('|'); row++) { + const values = cells(lines[row]); + const skills = extractLinks(values[column] ?? '') + .map((link) => resolveLink(fromFile, link.target)?.file ?? '') + .map((file) => SKILL_LINK.exec(file)?.[1]) + .filter((skill): skill is string => Boolean(skill)); + rows.push({ line: row + 1, task: values[0].toLowerCase().replace(/\s+/g, ' '), skills }); + } + } + return rows; +} + +/** Parses the standardized reference frontmatter. Returns an error message for any deviation. */ +export function parseFrontmatter(markdown: string): Frontmatter | string { + const lines = markdown.split('\n'); + if (lines[0] !== '---') { + return 'missing frontmatter; the first line must be ---'; + } + const idLine = /^id: (\S+)$/.exec(lines[1] ?? ''); + const kindLine = /^kind: (\S+)$/.exec(lines[2] ?? ''); + if (!idLine || !kindLine || lines[3] !== 'owner_paths:') { + return 'frontmatter must contain exactly id, kind, and owner_paths in that order'; + } + const ownerPaths: string[] = []; + let index = 4; + while (index < lines.length && /^ {2}- \S+$/.test(lines[index])) { + ownerPaths.push(lines[index].slice(4)); + index++; + } + if (lines[index] !== '---') { + return 'frontmatter must end after the owner_paths list; no other keys are allowed'; + } + if (ownerPaths.length === 0) { + return 'owner_paths must list at least one path'; + } + return { id: idLine[1], kind: kindLine[1], ownerPaths, bodyStart: index + 1 }; +} + +function parseSkillFrontmatter(markdown: string): Record | undefined { + const match = /^---\n([\s\S]*?)\n---\n/.exec(markdown); + if (!match) { + return undefined; + } + const fields: Record = {}; + for (const line of match[1].split('\n')) { + const field = /^([a-z-]+):\s*(.*)$/.exec(line); + if (field) { + fields[field[1]] = field[2].trim(); + } + } + return fields; +} + +function isInstructionSurface(file: string): boolean { + return ( + path.posix.basename(file) === 'AGENTS.md' || + file === 'CLAUDE.md' || + file === '.github/copilot-instructions.md' || + (file.startsWith(`${SKILLS_DIR}/`) && file.endsWith('.md')) || + (file.startsWith(`${REFERENCES_DIR}/`) && file.endsWith('.md')) + ); +} + +function requiresFrontmatter(file: string): boolean { + return ( + file.endsWith('.md') && + (file.startsWith(`${REFERENCES_DIR}/`) || new RegExp(`^${SKILLS_DIR.replace('.', '\\.')}/[^/]+/references/`).test(file)) + ); +} + +function resolveLink(fromFile: string, target: string): { file: string; fragment?: string } | undefined { + if (/^[a-z][a-z0-9+.-]*:/i.test(target)) { + return undefined; + } + const [rawPath, fragment] = target.split('#', 2); + let decoded: string; + try { + decoded = decodeURIComponent(rawPath); + } catch { + decoded = rawPath; + } + const file = !decoded + ? fromFile + : decoded.startsWith('/') + ? path.posix.normalize(decoded.slice(1)) + : path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), decoded)); + return { file, fragment: fragment || undefined }; +} + +/** Runs every check against the repository at `root`. */ +export function checkAgentInstructions(root: string): CheckResult { + const issues: Issue[] = []; + const files = listFiles(root); + const read = (file: string) => readFileSync(path.join(root, file), 'utf8'); + const surfaces = files.filter(isInstructionSurface).sort(); + const anchorCache = new Map>(); + const anchorsOf = (file: string) => { + let anchors = anchorCache.get(file); + if (!anchors) { + anchors = collectAnchors(read(file)); + anchorCache.set(file, anchors); + } + return anchors; + }; + + // Frontmatter, IDs, and kinds. + const frontmatter = new Map(); + const ids = new Map(); + for (const file of surfaces.filter(requiresFrontmatter)) { + const parsed = parseFrontmatter(read(file)); + if (typeof parsed === 'string') { + issues.push({ file, line: 1, message: parsed }); + continue; + } + frontmatter.set(file, parsed); + if (!ID_PATTERN.test(parsed.id)) { + issues.push({ file, line: 2, message: `invalid id "${parsed.id}"` }); + } + const previous = ids.get(parsed.id); + if (previous) { + issues.push({ file, line: 2, message: `duplicate id "${parsed.id}" (also in ${previous})` }); + } + ids.set(parsed.id, file); + if (!KINDS.has(parsed.kind)) { + issues.push({ file, line: 3, message: `unknown kind "${parsed.kind}"` }); + } + for (const owner of parsed.ownerPaths) { + if (!existsExact(root, owner)) { + issues.push({ file, message: `owner path does not exist: ${owner}` }); + } + } + } + + // Index completeness. + const indexFile = `${REFERENCES_DIR}/README.md`; + if (files.includes(indexFile)) { + const index = read(indexFile); + const indexed = new Set( + extractLinks(index) + .map((link) => resolveLink(indexFile, link.target)?.file) + .filter(Boolean), + ); + for (const [file, meta] of frontmatter) { + if (file.startsWith(`${REFERENCES_DIR}/`) && INDEXED_KINDS.has(meta.kind)) { + if (!indexed.has(file)) { + issues.push({ file, message: `not linked from ${indexFile}` }); + } + if (!index.includes(`\`${meta.id}\``)) { + issues.push({ file, message: `id "${meta.id}" is missing from the ${indexFile} document index` }); + } + } + } + } else if (frontmatter.size > 0) { + issues.push({ file: indexFile, message: 'missing reference index' }); + } + + // Compatibility references must point directly at a live document. + for (const [file, meta] of frontmatter) { + if (meta.kind !== 'compatibility-reference') { + continue; + } + const targets = extractLinks(read(file)) + .map((link) => resolveLink(file, link.target)?.file) + .filter((target): target is string => target !== undefined && target.endsWith('.md') && existsExact(root, target)); + if (targets.length === 0) { + issues.push({ file, message: 'compatibility reference must link to its replacement' }); + } else if (targets.every((target) => frontmatter.get(target)?.kind === 'compatibility-reference')) { + issues.push({ file, message: 'compatibility reference links only to another compatibility reference' }); + } + } + + // Skills. + const skillNames = new Map(); + for (const file of surfaces.filter((f) => /^\.github\/skills\/[^/]+\/SKILL\.md$/.test(f))) { + const dir = file.split('/')[2]; + const fields = parseSkillFrontmatter(read(file)); + if (!fields) { + issues.push({ file, line: 1, message: 'missing skill frontmatter' }); + continue; + } + if (fields.name !== dir) { + issues.push({ file, line: 2, message: `skill name "${fields.name}" must match its directory "${dir}"` }); + } + if (skillNames.has(fields.name)) { + issues.push({ file, message: `duplicate skill name "${fields.name}"` }); + } + skillNames.set(fields.name, file); + const words = (fields.description ?? '').split(/\s+/).filter(Boolean).length; + if (words === 0) { + issues.push({ file, message: 'skill description is empty' }); + } else if (words > MAX_SKILL_DESCRIPTION_WORDS) { + issues.push({ file, message: `skill description has ${words} words; the limit is ${MAX_SKILL_DESCRIPTION_WORDS}` }); + } + } + + // Outbound links and code paths from instruction surfaces. + for (const file of surfaces) { + const text = read(file); + for (const link of extractLinks(text)) { + const resolved = resolveLink(file, link.target); + if (!resolved) { + continue; + } + if (!existsExact(root, resolved.file)) { + issues.push({ file, line: link.line, message: `broken link: ${link.target}` }); + continue; + } + if (resolved.fragment && resolved.file.endsWith('.md') && !anchorsOf(resolved.file).has(resolved.fragment)) { + issues.push({ file, line: link.line, message: `missing anchor: ${link.target}` }); + } + } + for (const codePath of extractCodePaths(text)) { + if (!existsExact(root, codePath.target) && !isGitIgnored(root, codePath.target)) { + issues.push({ file, line: codePath.line, message: `path in code span does not exist: ${codePath.target}` }); + } + } + } + + // Inbound fragments from any Markdown into instruction surfaces. + const surfaceSet = new Set(surfaces); + for (const file of files) { + if (!file.endsWith('.md') || surfaceSet.has(file) || path.posix.basename(file) === 'CHANGELOG.md') { + continue; + } + for (const link of extractLinks(read(file))) { + const resolved = resolveLink(file, link.target); + if (resolved?.fragment && surfaceSet.has(resolved.file) && !anchorsOf(resolved.file).has(resolved.fragment)) { + issues.push({ file, line: link.line, message: `missing anchor in instruction file: ${link.target}` }); + } + } + } + + // Compatibility references are terminal: nothing links to them except the redirects themselves. + const compatibility = new Set([...frontmatter].filter(([, meta]) => meta.kind === 'compatibility-reference').map(([file]) => file)); + for (const file of files) { + if (!file.endsWith('.md') || compatibility.has(file) || path.posix.basename(file) === 'CHANGELOG.md') { + continue; + } + for (const link of extractLinks(read(file))) { + const target = resolveLink(file, link.target)?.file; + if (target && compatibility.has(target)) { + issues.push({ file, line: link.line, message: `links to compatibility reference ${target}; link to its replacement instead` }); + } + } + } + + // Explicit prerequisites must be acyclic and shallow. + const prerequisites = new Map(); + for (const file of surfaces) { + const targets: string[] = []; + for (const link of extractPrerequisites(read(file))) { + const target = resolveLink(file, link.target)?.file; + if (target && target.endsWith('.md') && existsExact(root, target)) { + if (target === file) { + issues.push({ file, line: link.line, message: 'document lists itself as a prerequisite' }); + } else { + targets.push(target); + } + } + } + prerequisites.set(file, [...new Set(targets)]); + } + const depthMemo = new Map(); + const reportedCycles = new Set(); + const depth = (file: string, stack: string[]): number => { + const loop = stack.indexOf(file); + if (loop >= 0) { + const cycle = [...stack.slice(loop), file]; + const key = cycle.slice(0, -1).sort().join('|'); + if (!reportedCycles.has(key)) { + reportedCycles.add(key); + issues.push({ file, message: `prerequisite cycle: ${cycle.join(' -> ')}` }); + } + return 0; + } + const cached = depthMemo.get(file); + if (cached !== undefined) { + return cached; + } + const next = prerequisites.get(file) ?? []; + const result = next.length === 0 ? 0 : 1 + Math.max(...next.map((target) => depth(target, [...stack, file]))); + depthMemo.set(file, result); + return result; + }; + for (const file of surfaces) { + const chain = depth(file, []); + if (chain > MAX_PREREQUISITE_DEPTH) { + issues.push({ file, message: `prerequisite chain is ${chain} reads deep; the limit is ${MAX_PREREQUISITE_DEPTH}` }); + } + } + + // Routing tables send each task to exactly one skill, agree across routers, and cover every skill from the root. + const routedTasks = new Map(); + for (const file of surfaces) { + for (const row of extractRoutingRows(read(file), file)) { + if (row.skills.length !== 1) { + issues.push({ file, line: row.line, message: `routing row "${row.task}" must link exactly one skill; found ${row.skills.length}` }); + continue; + } + const previous = routedTasks.get(row.task); + if (previous && previous.skills !== row.skills[0]) { + issues.push({ + file, + line: row.line, + message: `task "${row.task}" routes to ${row.skills[0]} here but to ${previous.skills} in ${previous.file}`, + }); + } else if (!previous) { + routedTasks.set(row.task, { skills: row.skills[0], file }); + } + } + } + if (files.includes('AGENTS.md')) { + const rootRouted = new Set(extractRoutingRows(read('AGENTS.md'), 'AGENTS.md').flatMap((row) => row.skills)); + for (const [skill, file] of skillNames) { + if (!rootRouted.has(skill)) { + issues.push({ file, message: `skill "${skill}" is not routed from the root AGENTS.md routing table` }); + } + } + } + + // Rule definitions and citations. + const rules = new Map(); + for (const file of surfaces) { + read(file) + .split('\n') + .forEach((line, index) => { + const match = RULE_DEFINITION.exec(line); + if (match) { + const previous = rules.get(match[1]); + if (previous) { + issues.push({ file, line: index + 1, message: `rule ${match[1]} is already defined in ${previous}` }); + } else { + rules.set(match[1], `${file}:${index + 1}`); + } + } + }); + } + for (const file of surfaces) { + read(file) + .split('\n') + .forEach((line, index) => { + for (const match of line.matchAll(RULE_CITATION)) { + if (!rules.has(match[0])) { + issues.push({ file, line: index + 1, message: `undefined rule ID ${match[0]}` }); + } + } + }); + } + + // Evaluation fixtures. + const caseIds = new Set(); + for (const file of files.filter((f) => f.startsWith(`${EVAL_CASES_DIR}/`))) { + issues.push(...checkEvalCase(root, file, read(file), { caseIds, rules, skills: skillNames })); + } + + const report = [...ALWAYS_ON, ...skillNames.values()] + .filter((file) => files.includes(file)) + .map((file) => ({ file, bytes: statSync(path.join(root, file)).size })); + return { issues, report }; +} + +function checkEvalCase( + root: string, + file: string, + text: string, + context: { caseIds: Set; rules: Map; skills: Map }, +): Issue[] { + const issues: Issue[] = []; + const fail = (message: string) => issues.push({ file, message }); + if (!file.endsWith('.json')) { + fail('evaluation cases must be JSON files'); + return issues; + } + let data: Record; + try { + data = JSON.parse(text); + } catch (error) { + fail(`invalid JSON: ${(error as Error).message}`); + return issues; + } + const stem = path.posix.basename(file, '.json'); + const strings = (value: unknown): string[] | undefined => + Array.isArray(value) && value.every((item) => typeof item === 'string') ? (value as string[]) : undefined; + const checkPaths = (label: string, value: unknown) => { + const list = strings(value); + if (!list) { + fail(`${label} must be an array of strings`); + return []; + } + for (const item of list) { + if (!existsExact(root, item)) { + fail(`${label} path does not exist: ${item}`); + } + } + return list; + }; + + if (data.schema_version !== 1) fail('schema_version must be 1'); + if (data.case_id !== stem) fail(`case_id must match the file name "${stem}"`); + if (context.caseIds.has(stem)) fail(`duplicate case_id "${stem}"`); + context.caseIds.add(stem); + if (!SPLITS.has(data.split as string)) fail('split must be development or holdout'); + if (!TASK_MODES.has(data.task_mode as string)) fail(`unknown task_mode "${String(data.task_mode)}"`); + if (typeof data.prompt !== 'string' || data.prompt.trim() === '') fail('prompt must be a non-empty string'); + + const expected = (data.expected ?? {}) as Record; + const skills = strings(expected.skills); + if (!skills || skills.length === 0) { + fail('expected.skills must list at least one skill'); + } else { + for (const skill of skills) { + if (!context.skills.has(skill)) fail(`unknown skill "${skill}"`); + } + } + const classification = (expected.classification ?? {}) as Record; + if (!MODELS.has(classification.implementation_model as string)) { + fail(`unknown implementation_model "${String(classification.implementation_model)}"`); + } + if (!strings(classification.public_compatibility)) fail('expected.classification.public_compatibility must be an array of strings'); + const platforms = strings(classification.native_platforms); + if (!platforms) { + fail('expected.classification.native_platforms must be an array of strings'); + } else { + for (const platform of platforms) { + if (!PLATFORMS.has(platform)) fail(`unknown native platform "${platform}"`); + } + } + checkPaths('expected.required_reads', expected.required_reads); + const forbidden = strings(expected.forbidden_actions); + if (!forbidden) { + fail('expected.forbidden_actions must be an array of strings'); + } else { + for (const action of forbidden) { + if (!FORBIDDEN_ACTIONS.has(action)) fail(`unknown forbidden action "${action}"`); + } + } + + const assertions = data.assertions; + if (!Array.isArray(assertions) || assertions.length === 0) { + fail('assertions must be a non-empty array'); + return issues; + } + const assertionIds = new Set(); + for (const assertion of assertions as Record[]) { + const id = assertion.id as string; + if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id)) fail(`invalid assertion id "${String(id)}"`); + if (assertionIds.has(id)) fail(`duplicate assertion id "${id}"`); + assertionIds.add(id); + if (typeof assertion.description !== 'string' || assertion.description.trim() === '') { + fail(`assertion "${id}" needs a description`); + } + const ruleIds = strings(assertion.rule_ids) ?? []; + for (const rule of ruleIds) { + if (!context.rules.has(rule)) fail(`assertion "${id}" cites undefined rule ${rule}`); + } + const refs = checkPaths(`assertion "${id}" source_refs`, assertion.source_refs); + if (ruleIds.length === 0 && refs.length === 0) fail(`assertion "${id}" must cite a rule ID or a source path`); + } + return issues; +} diff --git a/scripts/src/tasks/checkAgentInstructionsTask.ts b/scripts/src/tasks/checkAgentInstructionsTask.ts new file mode 100644 index 00000000000..ec05ebf25e2 --- /dev/null +++ b/scripts/src/tasks/checkAgentInstructionsTask.ts @@ -0,0 +1,36 @@ +import { Command, Option } from 'clipanion'; +import { findGitRoot } from 'workspace-tools'; + +import { checkAgentInstructions } from './agentInstructions.ts'; + +export class CheckAgentInstructionsCommand extends Command { + static override paths = [['check-agent-instructions']]; + + static override usage = Command.Usage({ + description: 'Check the integrity of agent instructions, skills, shared references, and evaluation cases', + details: + 'Validates reference frontmatter, links and anchors, code-span paths, skill metadata, rule IDs, the reference index, and evaluation fixtures. See references/agents/README.md.', + examples: [['Check agent instructions', '$0 check-agent-instructions']], + }); + + verbose = Option.Boolean('--verbose', false, { description: 'Report always-on instruction and skill sizes' }); + + async execute() { + const root = findGitRoot(process.cwd()); + const { issues, report } = checkAgentInstructions(root); + if (this.verbose) { + for (const entry of report) { + console.log(`${entry.bytes.toString().padStart(7)} bytes ${entry.file}`); + } + } + for (const issue of issues) { + console.error(`${issue.file}${issue.line ? `:${issue.line}` : ''}: ${issue.message}`); + } + if (issues.length > 0) { + console.error(`Agent instruction check failed with ${issues.length} issue(s).`); + return 1; + } + console.log('Agent instruction check passed.'); + return 0; + } +}