Update AI development principles from SSOT (20260512)

Summary

This MR updates AI development principles based on recent changes to the development documentation (SSOT).

Updated principles and their source-doc changes

database-migrations

Source files (4)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/database-migrations.md b/.ai/principles/baselines/database-migrations.md
new file mode 100644
index 000000000..0a2ccdbc0
--- /dev/null
+++ b/.ai/principles/baselines/database-migrations.md
@@ -0,0 +1,16 @@
+### Schema Migrations
+
+- Files in `db/schema_migrations/` are auto-generated and do not require a newline at the end -- do not flag missing newlines
+
+## BBM doc YAML required fields
+
+When creating a `db/docs/batched_background_migrations/<name>.yml`, the YAML MUST include:
+
+- `migration_job_name: <BBM class name in CamelCase>`
+- `description: <one-line description>`
+- `feature_category: <category symbol>`
+- `introduced_by_url: <MR URL>` (placeholder OK for unreleased)
+- `milestone: '<X.Y>'`
+- `queued_migration_version: <version timestamp>`
+- `gitlab_schema: <gitlab_main | gitlab_ci | gitlab_main_user | gitlab_main_org>` — match the schema of the BBM's primary table
+- (optional, post-finalize) `finalized_by: <version>`

database-schema

Source files (12)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/database-schema.md b/.ai/principles/baselines/database-schema.md
new file mode 100644
index 000000000..4474173d2
--- /dev/null
+++ b/.ai/principles/baselines/database-schema.md
@@ -0,0 +1,5 @@
+### Index Removal
+
+- Before removing an index, verify queries can use other existing indexes efficiently
+- Confirm the index is unused on GitLab.com and Self-Managed
+- For investigating index usage, check Grafana dashboards for index usage data

database-queries

Source files (8)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/database-queries.md b/.ai/principles/baselines/database-queries.md
new file mode 100644
index 000000000..36f3c299c
--- /dev/null
+++ b/.ai/principles/baselines/database-queries.md
@@ -0,0 +1,21 @@
+### ActiveRecord Scopes
+
+- New or modified scopes should have appropriate indexes for filtered columns
+- Scopes should avoid expensive operations like subqueries on large tables
+- Default scopes are avoided unless absolutely necessary (they can cause unexpected query behavior)
+
+### Partitioned Tables
+
+- Leverage partition pruning wherever possible when querying partitioned tables to minimize LWLock contention
+
+### Query Plan Analysis
+
+- Analyze query plans in the MR description for:
+  - Sequential scans on large tables
+  - Nested loops with large datasets
+  - Missing or inefficient index usage
+  - High-cost operations
+  - Unexpected sort operations
+  - Verify the query returns expected records (not zero rows)
+  - Check that maximum query execution time is under 100ms
+  - Ensure the query plan reflects the complete query as executed (including all chained scopes, pagination, ordering)

clickhouse

Source files (1)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/clickhouse.md b/.ai/principles/baselines/clickhouse.md
new file mode 100644
index 000000000..55741f3d2
--- /dev/null
+++ b/.ai/principles/baselines/clickhouse.md
@@ -0,0 +1,16 @@
+### Verify Before Flagging
+
+When a diff modifies or replaces an existing structure, always verify the current state from an
+authoritative source before flagging a discrepancy. Never infer the pre-change state solely from
+diff context — check the actual source of truth. For example:
+
+- **Migration `down` methods**: verify the `down` schema against the actual pre-migration schema by
+  querying the local ClickHouse database (`SHOW CREATE TABLE tablename`) or, if unavailable, reading
+  the schema from the base branch (`git show master:db/click_house/main.sql`). Compare
+  column-by-column: names, types, defaults, engine, primary key, ORDER BY, and SETTINGS.
+- **Table recreation** (`DROP TABLE IF EXISTS` + `CREATE TABLE`): verify the old table definition
+  the same way before claiming columns or settings are missing.
+
+### Schema Migration Files
+
+- Files in `db/click_house/schema_migrations/` are auto-generated and do not require a newline at the end — do not flag missing newlines

security

Source files (6)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/doc/development/secure_coding_guidelines/_index.md b/doc/development/secure_coding_guidelines/_index.md
index a03f7d6a2..20ffd3dbc 100644
--- a/doc/development/secure_coding_guidelines/_index.md
+++ b/doc/development/secure_coding_guidelines/_index.md
@@ -890,7 +890,7 @@ This sensitive data must be handled carefully to avoid leaks which could lead to
   - The [Gitleaks Git hook](https://gitlab.com/gitlab-com/gl-security/security-research/gitleaks-endpoint-installer) is recommended for preventing credentials from being committed.
 - Never log credentials under any circumstance. Issue [#353857](https://gitlab.com/gitlab-org/gitlab/-/issues/353857) is an example of credential leaks through log file.
 - When credentials are required in a CI/CD job, use [masked variables](../../ci/variables/_index.md#mask-a-cicd-variable) to help prevent accidental exposure in the job logs. Be aware that when [debug logging](../../ci/variables/variables_troubleshooting.md#enable-debug-logging) is enabled, all masked CI/CD variables are visible in job logs. Also consider using [protected variables](../../ci/variables/_index.md#protect-a-cicd-variable) when possible so that sensitive CI/CD variables are only available to pipelines running on protected branches or protected tags.
-- Proper scanners must be enabled depending on what data those credentials are protecting. See the [Application Security Inventory Policy](https://handbook.gitlab.com/handbook/security/product-security/application-security/inventory/#policies) and our [Data Classification Standards](https://handbook.gitlab.com/handbook/security/data-classification-standard/#standard).
+- Proper scanners must be enabled depending on what data those credentials are protecting. See the [Application Security Inventory Policy](https://handbook.gitlab.com/handbook/security/product-security/security-platforms-architecture/application-security/inventory/#policies) and our [Data Classification Standards](https://handbook.gitlab.com/handbook/security/data-classification-standard/#standard).
 - To store and/or share credentials between teams, refer to [1Password for Teams](https://handbook.gitlab.com/handbook/security/password-guidelines/#1password-for-teams) and follow [the 1Password Guidelines](https://handbook.gitlab.com/handbook/security/password-guidelines/#1password-guidelines).
 - If you need to share a secret with a team member, use 1Password. Do not share a secret over email, Slack, or other service on the Internet.
 
@@ -1183,4 +1183,4 @@ vulnerability would be a critical (severity 1) incident.
 ## Who to contact if you have questions
 
 For general guidance, contact the
-[Application Security](https://handbook.gitlab.com/handbook/security/product-security/application-security/) team.
+[Application Security](https://handbook.gitlab.com/handbook/security/product-security/security-platforms-architecture/application-security/) team.

authentication

Source files (2)

code-review

Source files (1)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 4fb13a249..a534a2374 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -282,8 +282,8 @@ For further quality guidelines, see [testing](https://handbook.gitlab.com/handbo
 
 ##### Security
 
-1. You have confirmed that if this MR contains changes to processing or storing of credentials or tokens, authorization, and authentication methods, or other items described in [the security review guidelines](https://handbook.gitlab.com/handbook/security/product-security/application-security/appsec-reviews/#what-should-be-reviewed), you have added the `~security` label and you have `@`-mentioned `@gitlab-com/gl-security/appsec`.
-1. You have reviewed the documentation regarding [internal application security reviews](https://handbook.gitlab.com/handbook/security/product-security/application-security/appsec-reviews/#internal-application-security-reviews) for **when** and **how** to request a security review and requested a security review if this is warranted for this change.
+1. You have confirmed that if this MR contains changes to processing or storing of credentials or tokens, authorization, and authentication methods, or other items described in [the security review guidelines](https://handbook.gitlab.com/handbook/security/product-security/security-platforms-architecture/application-security/appsec-reviews/#what-should-be-reviewed), you have added the `~security` label and you have `@`-mentioned `@gitlab-com/gl-security/appsec`.
+1. You have reviewed the documentation regarding [internal application security reviews](https://handbook.gitlab.com/handbook/security/product-security/security-platforms-architecture/application-security/appsec-reviews/#internal-application-security-reviews) for **when** and **how** to request a security review and requested a security review if this is warranted for this change.
 1. If there are security scan results that are blocking the MR (due to the [merge request approval policies](https://gitlab.com/gitlab-com/gl-security/security-policies)):
    - For true positive findings, they should be corrected before the merge request is merged. This will remove the AppSec approval required by the merge request approval policy.
    - For false positive findings, something that should be discussed for risk acceptance, or anything questionable, ping `@gitlab-com/gl-security/appsec`.

backend-ruby

Source files (11)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/backend-ruby.md b/.ai/principles/baselines/backend-ruby.md
new file mode 100644
index 000000000..3f27e9381
--- /dev/null
+++ b/.ai/principles/baselines/backend-ruby.md
@@ -0,0 +1,6 @@
+### Log Field Standards
+
+- Log fields should be defined within the LabKit Ruby Fields module
+- Common logging fields imported from `labkit-ruby` `lib/labkit/fields.rb`
+- New fields added to log messages must not be dynamically generated
+- Follow Field Standardisation Guidelines for observability
diff --git a/doc/development/backend/ruby_style_guide.md b/doc/development/backend/ruby_style_guide.md
index 217b0315a..db409f06e 100644
--- a/doc/development/backend/ruby_style_guide.md
+++ b/doc/development/backend/ruby_style_guide.md
@@ -411,18 +411,20 @@ Timelogs.for_project(project)
 
 #### `with_`
 
-For scopes which `joins`, `includes`, or filters `where(has_one: record)` or `where(has_many: record)` or `where(boolean condition)`
+For scopes that use `joins`, or filters `where(has_one: record)` or `where(has_many: record)`
+or `where(boolean condition)` where the result set changes.
+
 For example:
 
 ```ruby
-scope :with_labels, -> { includes(:labels) }
-AbuseReport.with_labels
-
 scope :with_status, ->(status) { where(status: status) }
 Clusters::AgentToken.with_status(:active)
 
 scope :with_due_date, -> { where.not(due_date: nil) }
 Issue.with_due_date
+
+scope :with_runner_type, ->(type) { joins(:runner).where(runner: { runner_type: type }) }
+Ci::Build.with_runner_type(:instance_type)
 ```
 
 It is also fine to use custom scope names, for example:
@@ -432,6 +434,38 @@ scope :undeleted, -> { where('policy_index >= 0') }
 Security::Policy.undeleted
 ```
 
+#### `including_`
+
+For scopes that eager load associations using `includes`. The result set does not change.
+Use `including_` to avoid N+1 queries when you do not need to control the SQL loading strategy.
+ActiveRecord decides whether to use a JOIN or a subquery.
+
+For example:
+
+```ruby
+scope :including_tags, -> { includes(:tags) }
+Package.including_tags
+
+scope :including_project, -> { includes(:project) }
+Issue.including_project
+```
+
+#### `preload_`
+
+For scopes that eager load associations using `preload`. The result set does not change.
+Use instead of `including_` when loading multiple `has_many` associations, or when a
+separate subquery is explicitly required.
+
+For example:
+
+```ruby
+scope :preload_author, -> { preload(:author) }
+MergeRequest.preload_author
+
+scope :preload_access_levels, -> { preload(:push_access_levels, :merge_access_levels, :unprotect_access_levels) }
+ProtectedBranch.preload_access_levels
+```
+
 #### `order_by_`
 
 For scopes which `order`.
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 52b11cc38..fe186e943 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -410,137 +410,8 @@ use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make sure to
 
 ### Plurals
 
-- In Ruby/HAML:
-
-  ```ruby
-  n_('Apple', 'Apples', 3)
-  # => 'Apples'
-  ```
-
-  Using interpolation:
-
-  ```ruby
-  n_("There is a mouse.", "There are %d mice.", size) % size
-  # => When size == 1: 'There is a mouse.'
-  # => When size == 2: 'There are 2 mice.'
-  ```
-
-  Avoid using `%d` or count variables in singular strings. This allows more natural translation in
-  some languages.
-- In JavaScript:
-
-  ```javascript
-  n__('Apple', 'Apples', 3)
-  // => 'Apples'
-  ```
-
-  Using interpolation:
-
-  ```javascript
-  n__('Last day', 'Last %d days', x)
-  // => When x == 1: 'Last day'
-  // => When x == 2: 'Last 2 days'
-  ```
-
-- In Vue:
-
-  One of [the recommended ways to organize translated strings for Vue files](#vue-files) is to extract them into a `constants.js` file.
-  That can be difficult to do when there are pluralized strings because the `count` variable won't be known inside the constants file.
-  To overcome this, we recommend creating a function which takes a `count` argument:
-
-  ```javascript
-  // .../feature/constants.js
-  import { n__ } from '~/locale';
-
-  export const I18N = {
-    // Strings that are only singular don't need to be a function
-    someDaysRemain: __('Some days remain'),
-    daysRemaining(count) { return n__('%d day remaining', '%d days remaining', count); },
-  };
-  ```
-
-  Then within a Vue component the function can be used to retrieve the correct pluralization form of the string:
-
-  ```javascript
-  // .../feature/components/days_remaining.vue
-  import { sprintf } from '~/locale';
-  import { I18N } from '../constants';
-
-  <script>
-    export default {
-      props: {
-        days: {
-          type: Number,
-          required: true,
-        },
-      },
-      i18n: I18N,
-    };
-  </script>
-
-  <template>
-    <div>
-      <span>
-        A singular string:
-        {{ $options.i18n.someDaysRemain }}
-      </span>
-      <span>
-        A plural string:
-        {{ $options.i18n.daysRemaining(days) }}
-      </span>
-    </div>
-  </template>
-  ```
-
-The `n_` and `n__` methods should only be used to fetch pluralized translations of the same
-string, not to control the logic of showing different strings for different
-quantities. For similar strings, pluralize the entire sentence to provide the most context
-when translating. Some languages have different quantities of target plural forms.
-For example, Chinese (simplified) has only one target plural form in our
-translation tool. This means the translator has to choose to translate only one
-of the strings, and the translation doesn't behave as intended in the other case.
-
-Below are some examples:
-
-Example 1: For different strings
-
-Use this:
-
-```ruby
-if selected_projects.one?
-  selected_projects.first.name
-else
-  n_("Project selected", "%d projects selected", selected_projects.count)
-end
-```
-
-Instead of this:
-
-```ruby
-# incorrect usage example
-format(n_("%{project_name}", "%d projects selected", count), project_name: 'GitLab')
-```
-
-Example 2: For similar strings

... (diff truncated at 200 lines / 8 KB; use the per-file commit-history links above for the full picture)

backend-architecture

Source files (6)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/backend-architecture.md b/.ai/principles/baselines/backend-architecture.md
new file mode 100644
index 000000000..4685866ff
--- /dev/null
+++ b/.ai/principles/baselines/backend-architecture.md
@@ -0,0 +1,18 @@
+### CE/EE Code Separation
+
+- CE code (outside `ee/`) must not directly reference `EE::` namespaced classes
+- EE extensions use `prepend_mod` pattern in CE files
+- If CE code needs EE-aware behavior, use `prepend_mod` hooks or `Gitlab.ee?` guards
+- Flag direct references to `EE::` namespaced classes in CE code (prevents FOSS build failures)
+
+### ActiveRecord Callbacks
+
+- Callbacks should only modify data on the current model, not associated records
+- Question if callback logic should be in a service layer instead
+- Flag callbacks with side effects (external API calls, updating other records, complex business logic)
+- Flag bulk operations on associated records in callbacks (performance concern as associations grow)
+- Acceptable uses: data normalization on current model only (trimming whitespace, setting defaults)
+
+### Authorization
+
+- Before changing authorization logic, read the existing `authorize!` / `authorize_admin!` call and verify what permission it currently enforces; the required fix may be documentation- or test-only with no code change needed

backend-ee

Source files (1)

rest-api

Source files (1)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/rest-api.md b/.ai/principles/baselines/rest-api.md
new file mode 100644
index 000000000..0b9a2bc68
--- /dev/null
+++ b/.ai/principles/baselines/rest-api.md
@@ -0,0 +1,9 @@
+### API Documentation Format
+
+- Every method must include the REST API request with HTTP method (GET, PUT, DELETE) followed by the request path starting with `/`
+- Every method must have a detailed description of attributes in a table format, with required attributes listed first, then sorted alphabetically
+- Every method must include a cURL example using `https://gitlab.example.com/api/v4/` as the endpoint and `<your_access_token>` as the token placeholder
+- Every method must have a detailed description of the response body and a JSON response example
+- If endpoint attributes are available only to higher subscription tiers or specific offerings, include this information in the attribute description
+- For complex object types, represent sub-attributes with dots, like `project.name` or `projects[].name` for arrays
+- For cURL commands: use long option names (`--header` instead of `-H`), declare URLs with the `--url` parameter in double quotes, and use line breaks with `\` for readability

graphql

Source files (4)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index af43c1d8f..289cfb5bc 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -30,22 +30,6 @@ This does not mitigate the problem on GitLab.com. New GraphQL fields still need
 
 You can use the `@gl_introduced` directive on any field, for example:
 
-<table>
-<thead>
-  <tr>
-    <td>
-      Query
-    </td>
-    <td>
-      Response
-    </td>
-  </tr>
-</thead>
-
-<tbody>
-<tr>
-<td>
-
 ```graphql
 fragment otherFieldsWithFuture on Namespace {
   webUrl
@@ -62,8 +46,7 @@ query namespaceWithFutureFields {
 }
 ```
 
-</td>
-<td>
+Response:
 
 ```json
 {
@@ -79,32 +62,11 @@ query namespaceWithFutureFields {
 }
 ```
 
-</td>
-</tr>
-</tbody>
-</table>
-
 You shouldn't use the directive with:
 
 - Arguments: Executable directives don't support arguments.
 - Fragments: Instead, use the directive in the fragment nodes.
-- Single future fields, in the query or in objects:
-
-<table>
-<thead>
-  <tr>
-    <td>
-      Query
-    </td>
-    <td>
-      Response
-    </td>
-  </tr>
-</thead>
-
-<tbody>
-<tr>
-<td>
+- Single future fields, in the query or in objects, for example:
 
   ```graphql
   query fetchData {
@@ -112,8 +74,7 @@ You shouldn't use the directive with:
   }
   ```
 
-</td>
-<td>
+  Response:
 
   ```json
   {
@@ -147,11 +108,7 @@ You shouldn't use the directive with:
   }
   ```
 
-</td>
-</tr>
-
-<tr>
-<td>
+  Query:
 
   ```graphql
   query fetchData {
@@ -161,8 +118,7 @@ You shouldn't use the directive with:
   }
   ```
 
-</td>
-<td>
+  Response:
 
   ```json
   {
@@ -195,12 +151,8 @@ You shouldn't use the directive with:
     ]
   }
   ```
-
-</td>
-</tr>
-
-<tr>
-<td>
+  
+  Query:
 
   ```graphql
   query fetchData {
@@ -210,8 +162,7 @@ You shouldn't use the directive with:
   }
   ```
 
-</td>
-<td>
+  Response:
 
   ```json
   {
@@ -246,12 +197,6 @@ You shouldn't use the directive with:
   }
   ```
 
-</td>
-</tr>
-
-</tbody>
-</table>
-
 ##### Non-nullable fields
 
 Future fields fallback to `null` when they don't exist in the backend. This means that non-nullable

workers

Source files (6)

frontend-vue

Source files (9)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/frontend-vue.md b/.ai/principles/baselines/frontend-vue.md
new file mode 100644
index 000000000..4a84b8fb8
--- /dev/null
+++ b/.ai/principles/baselines/frontend-vue.md
@@ -0,0 +1,21 @@
+### Pajamas Component Usage
+
+- Flag component usage that appears inconsistent with Pajamas "when to use" and "when not to use" guidelines
+- Flag usage of container components purely for simple visual separation without using the component's structural features (header, footer, etc.)
+- For simple visual separation without structured content, prefer utility classes (e.g., `gl-border gl-rounded-lg gl-p-5`) over container components
+- When both control and variants are toggled in Vue components layer, prefer the `<gitlab-experiment>` component
+
+### Experiments
+
+- Experiment uses an `experiment` type feature flag (not `development` or `ops`)
+- Context is appropriate and consistent (e.g., `actor:`, `project:`, `group:`)
+- Variants are clearly defined (control, candidate, or named variants)
+- Tracking calls use the same context as experiment runs
+- Frontend or feature tests exist to prevent premature code removal
+- Tests cover experiment variants and tracking behavior
+- Temporary assets (icons/illustrations) are in `/ee/app/assets/images` or `/app/assets/images`, not Pajamas library
+
+### Internationalization (i18n)
+
+- DO NOT split a translatable sentence across multiple `GlSprintf` instances; keep the full sentence (e.g., `"Created %{date} by %{author}"`) in a single `GlSprintf :message` so translators can reorder words across languages
+- Extract translation strings to a static `i18n` object on the Vue component (e.g., `$options.i18n.myString`) instead of inlining `s__()` / `__()` calls directly in `<template>`
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 52b11cc38..fe186e943 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -410,137 +410,8 @@ use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make sure to
 
 ### Plurals
 
-- In Ruby/HAML:
-
-  ```ruby
-  n_('Apple', 'Apples', 3)
-  # => 'Apples'
-  ```
-
-  Using interpolation:
-
-  ```ruby
-  n_("There is a mouse.", "There are %d mice.", size) % size
-  # => When size == 1: 'There is a mouse.'
-  # => When size == 2: 'There are 2 mice.'
-  ```
-
-  Avoid using `%d` or count variables in singular strings. This allows more natural translation in
-  some languages.
-- In JavaScript:
-
-  ```javascript
-  n__('Apple', 'Apples', 3)
-  // => 'Apples'
-  ```
-
-  Using interpolation:
-
-  ```javascript
-  n__('Last day', 'Last %d days', x)
-  // => When x == 1: 'Last day'
-  // => When x == 2: 'Last 2 days'
-  ```
-
-- In Vue:
-
-  One of [the recommended ways to organize translated strings for Vue files](#vue-files) is to extract them into a `constants.js` file.
-  That can be difficult to do when there are pluralized strings because the `count` variable won't be known inside the constants file.
-  To overcome this, we recommend creating a function which takes a `count` argument:
-
-  ```javascript
-  // .../feature/constants.js
-  import { n__ } from '~/locale';
-
-  export const I18N = {
-    // Strings that are only singular don't need to be a function
-    someDaysRemain: __('Some days remain'),
-    daysRemaining(count) { return n__('%d day remaining', '%d days remaining', count); },
-  };
-  ```
-
-  Then within a Vue component the function can be used to retrieve the correct pluralization form of the string:
-
-  ```javascript
-  // .../feature/components/days_remaining.vue
-  import { sprintf } from '~/locale';
-  import { I18N } from '../constants';
-
-  <script>
-    export default {
-      props: {
-        days: {
-          type: Number,
-          required: true,
-        },
-      },
-      i18n: I18N,
-    };
-  </script>
-
-  <template>
-    <div>
-      <span>
-        A singular string:
-        {{ $options.i18n.someDaysRemain }}
-      </span>
-      <span>
-        A plural string:
-        {{ $options.i18n.daysRemaining(days) }}
-      </span>
-    </div>
-  </template>
-  ```
-
-The `n_` and `n__` methods should only be used to fetch pluralized translations of the same
-string, not to control the logic of showing different strings for different
-quantities. For similar strings, pluralize the entire sentence to provide the most context
-when translating. Some languages have different quantities of target plural forms.
-For example, Chinese (simplified) has only one target plural form in our
-translation tool. This means the translator has to choose to translate only one
-of the strings, and the translation doesn't behave as intended in the other case.
-
-Below are some examples:
-
-Example 1: For different strings
-
-Use this:
-
-```ruby
-if selected_projects.one?
-  selected_projects.first.name
-else
-  n_("Project selected", "%d projects selected", selected_projects.count)
-end
-```
-
-Instead of this:
-
-```ruby
-# incorrect usage example
-format(n_("%{project_name}", "%d projects selected", count), project_name: 'GitLab')
-```
-
-Example 2: For similar strings
-
-Use this:
-
-```ruby
-n__('Last day', 'Last %d days', days.length)
-```
-
-Instead of this:
-
-```ruby
-# incorrect usage example
-const pluralize = n__('day', 'days', days.length)
-
-if (days.length === 1 ) {
-  return sprintf(s__('Last %{pluralize}', pluralize)
-}
-
-return sprintf(s__('Last %{dayNumber} %{pluralize}'), { dayNumber: days.length, pluralize })
-```
+GitLab uses GNU gettext for pluralization. For guidance on `n_()` and `n__()` syntax,
+[CLDR](https://cldr.unicode.org/) plural categories, and common anti-patterns, see [pluralization](pluralization.md).
 
 ### Namespaces
 

frontend-style

Source files (3)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/frontend-style.md b/.ai/principles/baselines/frontend-style.md
new file mode 100644
index 000000000..bb283f7fc
--- /dev/null
+++ b/.ai/principles/baselines/frontend-style.md
@@ -0,0 +1,5 @@
+### Pajamas CSS Overrides
+
+- Flag excessive CSS overrides on Pajamas components (multiple class overrides changing borders, backgrounds, padding, or other default styling)
+- Flag hardcoded color values, spacing values, or typography that should use design tokens
+- Flag fixed type scales (e.g., `gl-text-700-fixed`) used outside of Markdown contexts

frontend-haml

Source files (2)

frontend-a11y

Source files (2)

qa-rspec

Source files (5)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/qa-rspec.md b/.ai/principles/baselines/qa-rspec.md
new file mode 100644
index 000000000..273214bc8
--- /dev/null
+++ b/.ai/principles/baselines/qa-rspec.md
@@ -0,0 +1,24 @@
+### Branch Coverage
+
+- For conditional logic (`||`, `&&`, `if/else`, `case`), verify each branch has test coverage
+- For helper methods, ensure unit specs exist (not just integration coverage)
+- For ActiveRecord callbacks (`before_validation`, `before_save`), ensure unit specs test the callback behavior specifically
+
+### Edge Case Coverage
+
+- Flag missing edge case coverage for:
+  - Nil/empty values
+  - Boundary conditions
+  - Fallback logic (`||` operators)
+  - Error states
+
+### Feature Flags in Tests
+
+- DO NOT use `stub_feature_flags(flag: true)` — feature flags are enabled by default in the test environment, so stubbing to `true` is redundant and misleading
+- Only use `stub_feature_flags(flag: false)` to test the disabled code path
+- For the enabled case, write tests without any feature flag stub — the default state is already enabled
+
+### Spec File Paths
+
+- DO NOT create a new spec file in a subdirectory when a spec already exists at the canonical path (e.g., modify `spec/requests/api/pages_spec.rb`, DO NOT create `spec/requests/api/pages/pages_spec.rb`)
+- DO NOT remove existing shared examples when adding or fixing coverage; extend them or add new examples alongside

qa-jest

Source files (1)

performance

Source files (1)

documentation

Source files (2)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/documentation.md b/.ai/principles/baselines/documentation.md
new file mode 100644
index 000000000..9c93695e3
--- /dev/null
+++ b/.ai/principles/baselines/documentation.md
@@ -0,0 +1,34 @@
+### Common Mistakes - Repetition
+
+- Do not restate information already covered earlier in the same page or in a linked topic
+- Each section should add new information. Do not summarize what was just explained
+- Avoid restating the title or introduction in the first paragraph
+
+### Common Mistakes - Scope
+
+- Do not create a new page for a single concept, term, or procedure step
+
+### Common Mistakes - Accuracy
+
+- Only include information you can ground in the existing codebase, linked documentation, or content already on the page
+- Do not speculate or infer how a feature works
+- Do not invent command syntax, API parameters, or UI element names
+
+### Screenshot Guidelines
+
+- Resize wide or tall screenshots
+- Compress size on disk to 100 KB or less
+- Use descriptive lowercase filenames with underscores instead of hyphens
+- Filenames should include the major and minor version of GitLab in the format `_v18_6`
+
+### Content Guidelines
+
+- Avoid writing about the document itself; get straight to the point instead of using phrases like "This page shows"
+- Do not promise work in future milestones; instead say work is being proposed
+- Avoid blockquotes
+- When linking to GitLab issues, include the GitLab issue number in the link text
+- Start optional steps with "Optional."
+
+### Word List
+
+- Follow the GitLab Documentation recommended word list for consistent terminology

feature-flags

Source files (1)
SSOT diff since previous distillation (9ab16c7552964caf)
diff --git a/.ai/principles/baselines/feature-flags.md b/.ai/principles/baselines/feature-flags.md
new file mode 100644
index 000000000..c9b53327e
--- /dev/null
+++ b/.ai/principles/baselines/feature-flags.md
@@ -0,0 +1,8 @@
+### YAML Definition
+
+- DO NOT guess or hard-code the `milestone` value — read it from the `VERSION` file in the repo root and use the `MAJOR.MINOR` portion only (e.g., if `VERSION` contains `19.0.0-pre`, set `milestone: "19.0"`)
+
+### Experiments
+
+- If experiment uses only `experiment(:name, actor: current_user)` as context but the corresponding issue mentions tracking of namespace-based activation events, assignment should happen based on namespace or actor + namespace
+- If experiment is first assigned during registration, there should be another assignment tracking event with namespace context: `experiment(:name, actor: current_user).track(:assignment, namespace: group)`

analytics

Source files (1)

How this works

A scheduled pipeline detects changes to doc/development/ files listed in .ai/principles/manifest.yml, then uses the GitLab Duo Agent Platform Workflow API to distill updated principles for the affected domains.

Please review the checklist changes to ensure they accurately reflect the documentation updates.

Merge request reports

Loading