diff --git a/.claude/skills/update-feature-docs/SKILL.md b/.claude/skills/update-feature-docs/SKILL.md new file mode 100644 index 0000000000..418eff6aff --- /dev/null +++ b/.claude/skills/update-feature-docs/SKILL.md @@ -0,0 +1,78 @@ +--- +name: dd-sdk-ios:update-feature-docs +description: Use when public API changes have been made to review and update all *_FEATURE.md documentation files, or to audit whether they are still accurate. +--- + +# update-feature-docs + +Review and update the feature documentation files to match the current public API. + +## When to use + +- After adding or modifying a public API (configuration options, new types, new methods) +- Before opening a release PR, to make sure docs are in sync +- Any time you want to audit whether feature docs are still accurate + +## What this skill covers + +All `*_FEATURE.md` files in the repo. Each doc's frontmatter is the source of truth: +- `verified_against_commit` — the commit the doc was last verified against +- `tracked_files` — the public API source files whose changes should trigger a doc update + +To add a new feature doc to the system, just create a `*_FEATURE.md` file with the correct frontmatter — no script changes needed. + +## Steps + +1. **Discover feature docs** — find all `*_FEATURE.md` files in the repo (excluding `build/` and `artifacts/`). + +2. **For each doc, read its frontmatter** — extract `verified_against_commit` and `tracked_files`. + - If `tracked_files` is missing, derive the list from the doc's "Key Files" section (every source file path it references). Treat the doc as fully out of date and proceed to step 4 — the diff in step 3 cannot be computed. + - If `verified_against_commit` is missing, treat the doc as fully out of date and proceed to step 4 — the diff in step 3 cannot be computed. + - **Audit `tracked_files` coverage** against the doc's "Key Files" section. Every public-API source file path referenced in "Key Files" must also appear in `tracked_files`. This audit must happen *here*, before step 3 — otherwise drift in untracked files is silently ignored. If any are missing, add them to `tracked_files` and treat the doc as fully out of date so the newly-tracked files are inspected in step 4. + +3. **Get the diff since that commit** — run: + ``` + git diff ..HEAD -- + ``` + If there is no diff for a doc, it is up to date — skip steps 4–7 for that doc and move to the next one. **Do not skip step 8** — registry coverage must be checked even when every doc is fresh. + +4. **Read the current source files in full** — read each tracked source file to understand the current public API surface. + +5. **Compare against the feature doc** — identify every discrepancy: + - New configuration options or parameters missing from the doc + - Removed or renamed options still mentioned in the doc + - Changed defaults, behaviors, or platform availability + - New types, enums, or feature flags not documented + - **Deprecated public cases not documented** — walk every public enum case, method, and property, including those marked `@available(*, deprecated, message:)`. Deprecated cases stay on the public API and must appear in the doc with a clear deprecation note, otherwise customers reading the doc won't know they exist or that they're being phased out. + - Outdated code examples + +5b. **Audit every code snippet for compile-readiness** — for every constructor call, method call, and type reference in every code snippet (Quick Start and any inline examples): + - Locate the corresponding `init` / `func` / `struct` / `class` / `enum` declaration in the tracked source files (or in extension files providing convenience overloads). + - Confirm every parameter **without** a default value (`= ...`) is supplied in the snippet, with the correct label. + - Confirm parameter labels and argument ordering match the source. + - **Watch especially for newly-required parameters added to existing initializers.** A required parameter added to an existing `init` is the highest-risk drift — the constructor still looks "the same" at a glance, and the source diff is a one-line addition that is easy to skim past. Example regression: `DefaultSwiftUIRUMActionsPredicate(isLegacyDetectionEnabled:)` gained the required `isLegacyDetectionEnabled` argument and the snippet was not updated, causing a compile failure customers hit. + - Placeholder identifiers (e.g. ``, `MyCustomViewsPredicate`, `myView`) and user-defined helpers (e.g. `scrubURL`) are illustrative and need not resolve — only **SDK** API calls must be valid. + - If you find any mismatch, fix the snippet (this counts as a content update — proceed to step 6). + +6. **Update the feature doc** — apply all necessary changes: + - Update the Quick Start example to reflect the current API + - Update the Configuration Categories section + - Update Troubleshooting if relevant + - Fix any stale descriptions or defaults + +7. **Update the frontmatter** — set: + - `tracked_files` → if it was missing or out of date, write the list derived in step 2 + - `verified_against_commit` → current HEAD commit hash (use `git rev-parse --short=9 HEAD`) + - `sdk_version` → current version from `DatadogCore.podspec` + - `last_updated` → today's date (YYYY-MM-DD) + +8. **Update the registries** — when adding, renaming, or removing a `*_FEATURE.md` file, also update every place that hand-lists feature docs. `tools/feature-docs-verify.sh` enforces these and will fail CI otherwise: + - **`.github/workflows/changelog-to-confluence.yaml`** — both the `paths:` filter and the `cp` block. Use the relative path **without a leading slash** (`DatadogRUM/RUM_FEATURE.md`, not `/DatadogRUM/RUM_FEATURE.md`) — leading slashes silently never match in GitHub Actions `paths:` filters. The publish filename is kebab-case derived from the module + doc (`DatadogRUM/RUM_FEATURE.md` → `dd-sdk-ios-rum-feature.md`). + - **`AGENTS.md`** — add the doc to the file tree under "Feature-specific docs" and to the routing table ("Where to Look First"). + - **`docs/LLM_FEATURE_DOCS_GUIDELINES.md`** — add the doc to the expected feature-docs list. + +## Notes + +- Only update docs for features whose tracked source files actually changed. +- Do not rewrite sections that are still accurate — only fix what is wrong or missing. +- The Quick Start example should always compile against the current API. diff --git a/.github/workflows/changelog-to-confluence.yaml b/.github/workflows/changelog-to-confluence.yaml index 74749074b3..1dfb8be090 100644 --- a/.github/workflows/changelog-to-confluence.yaml +++ b/.github/workflows/changelog-to-confluence.yaml @@ -5,8 +5,8 @@ on: - master paths: - 'CHANGELOG.md' - - '/DatadogRUM/RUM_FEATURE.md' - - '/DatadogSessionReplay/SESSION_REPLAY_FEATURE.md' + - 'DatadogRUM/RUM_FEATURE.md' + - 'DatadogSessionReplay/SESSION_REPLAY_FEATURE.md' permissions: contents: read jobs: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d4b1add6a9..5095483e21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -137,6 +137,21 @@ API Surface Verify: - make clean repo-setup ENV=ci - make api-surface-verify +Feature Docs Verify: + stage: lint + rules: + - if: '$CI_COMMIT_BRANCH =~ /^(release|hotfix)\/.*/' + # tools/feature-docs-verify.sh runs `git diff ..HEAD`, + # which requires that commit to be reachable in the local history. GitLab CI + # shallow-clones at depth 20 by default, which would silently bury baselines + # for any feature doc that hasn't been touched in a few releases (e.g. Logs). + # The job only runs on release/hotfix branches (a handful of times per cycle), + # so the cost of a full clone is negligible. + variables: + GIT_DEPTH: "0" + script: + - make feature-docs-verify + Unit Tests (iOS): stage: test rules: diff --git a/CLAUDE.md b/CLAUDE.md index b1a524cf2c..7532ecda10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ Use these skills (via `/skill-name`) for common workflows: | `dd-sdk-ios:open-pr` | Opening a pull request against `develop` | | `dd-sdk-ios:running-tests` | Running unit, module, or integration tests | | `dd-sdk-ios:xcode-file-management` | Adding, removing, moving, or renaming Swift source files | +| `dd-sdk-ios:update-feature-docs` | Review and update all `*_FEATURE.md` docs after public API changes | ## CI Environment diff --git a/DatadogRUM/RUM_FEATURE.md b/DatadogRUM/RUM_FEATURE.md index dc571c14da..62320bccc8 100644 --- a/DatadogRUM/RUM_FEATURE.md +++ b/DatadogRUM/RUM_FEATURE.md @@ -1,7 +1,12 @@ --- -last_updated: 2025-01-03 -sdk_version: 3.3.0 -verified_against_commit: 1d3e80ec5 +last_updated: 2026-05-19 +sdk_version: 3.11.0 +verified_against_commit: d71d93183 +tracked_files: + - DatadogRUM/Sources/RUM.swift + - DatadogRUM/Sources/RUMConfiguration.swift + - DatadogRUM/Sources/RUMMonitor.swift + - DatadogRUM/Sources/RUMMonitorProtocol.swift --- # RUM (Real User Monitoring) Feature @@ -48,12 +53,14 @@ RUM.enable( // Default: nil (disabled) // Or use custom: MyCustomViewsPredicate() // Note: Also requires uiKitViewsPredicate for SwiftUI tracking to work correctly + // Note: Experimental API - may change in future releases swiftUIViewsPredicate: DefaultSwiftUIRUMViewsPredicate(), // SwiftUI automatic action tracking - provide predicate to enable // Default: nil (disabled) // Or use custom: MyCustomActionsPredicate() // Note: Also requires uiKitActionsPredicate for SwiftUI tracking to work correctly + // Note: Experimental API - behavior differs between iOS 17 and below vs iOS 18+ swiftUIActionsPredicate: DefaultSwiftUIRUMActionsPredicate(isLegacyDetectionEnabled: true), // Automatic network resource tracking - provide config to enable @@ -67,7 +74,14 @@ RUM.enable( // Optional: Add custom attributes to resources resourceAttributesProvider: { request, response, data, error in return ["custom.attribute": "value"] - } + }, + // Optional: Capture HTTP headers from requests and responses + // Default: .disabled + // Options: + // .disabled - No header capture + // .defaults - Capture predefined common headers (cache-control, content-type, etag, etc.) + // .custom([rules]) - Capture headers by custom rules + trackResourceHeaders: .defaults ), // Track user frustrations (error taps following errors) @@ -142,13 +156,21 @@ RUM.enable( // Default: true trackMemoryWarnings: true, + // Track slow frames / view hitches + // Default: true + trackSlowFrames: true, + // SDK telemetry sampling rate (for Datadog internal monitoring) // Default: 20.0 telemetrySampleRate: 20.0, // Collect accessibility settings in view events // Default: false - collectAccessibility: false + collectAccessibility: false, + + // Experimental feature flags (currently no active flags for RUM) + // Default: [:] + featureFlags: [:] ) ) @@ -199,14 +221,16 @@ monitor.stopView(key: "ProductList") ### Automatic Tracking Requires configuration to be set, otherwise disabled by default: -- **View tracking**: `uiKitViewsPredicate`, `swiftUIViewsPredicate` -- **Action tracking**: `uiKitActionsPredicate`, `swiftUIActionsPredicate` +- **View tracking**: `uiKitViewsPredicate`, `swiftUIViewsPredicate` *(SwiftUI: experimental)* +- **Action tracking**: `uiKitActionsPredicate`, `swiftUIActionsPredicate` *(SwiftUI: experimental, behavior differs on iOS 17 vs iOS 18+)* - **Resource tracking**: `urlSessionTracking` (automatic), optionally call `URLSessionInstrumentation.enableDurationBreakdown(with: .init(delegateClass: YourSessionDelegate.self))` for detailed timing +- **Header capture**: `urlSessionTracking.trackResourceHeaders` — `.disabled` (default), `.defaults` (common headers), or `.custom([rules])` ### Performance Monitoring - **Long tasks**: `longTaskThreshold` (default: 0.1s) - **App hangs**: `appHangThreshold` (default: nil/disabled) - **Vitals**: `vitalsUpdateFrequency` (default: .average) +- **Slow frames**: `trackSlowFrames` (default: true) — captures view hitches and attaches them to the corresponding RUM view ### Sampling - **Sessions**: `sessionSampleRate` (default: 100%) @@ -231,7 +255,7 @@ Event mappers allow modifying or dropping events before upload: ### "Views or actions not tracked" 1. Check if predicates are configured in RUMConfiguration 2. For UIKit: `uiKitViewsPredicate` and `uiKitActionsPredicate` must be set -3. For SwiftUI: `swiftUIViewsPredicate` and `swiftUIActionssPredicate` must be set, as well as UIKit predicates +3. For SwiftUI: `swiftUIViewsPredicate` and `swiftUIActionsPredicate` must be set, as well as UIKit predicates ### "Network requests not tracked" 1. Verify `urlSessionTracking` is configured in RUMConfiguration (RUM.enable() handles URLSessionInstrumentation internally) diff --git a/DatadogSessionReplay/SESSION_REPLAY_FEATURE.md b/DatadogSessionReplay/SESSION_REPLAY_FEATURE.md index bd038012e7..de194d7f51 100644 --- a/DatadogSessionReplay/SESSION_REPLAY_FEATURE.md +++ b/DatadogSessionReplay/SESSION_REPLAY_FEATURE.md @@ -1,7 +1,13 @@ --- -last_updated: 2025-01-03 -sdk_version: 3.3.0 -verified_against_commit: 1d3e80ec5 +last_updated: 2026-05-19 +sdk_version: 3.11.0 +verified_against_commit: d71d93183 +tracked_files: + - DatadogSessionReplay/Sources/SessionReplay.swift + - DatadogSessionReplay/Sources/SessionReplayConfiguration.swift + - DatadogSessionReplay/Sources/SessionReplayPrivacyOverrides.swift + - DatadogSessionReplay/Sources/SessionReplayPrivacyView.swift + - DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift --- # Session Replay Feature @@ -38,7 +44,7 @@ RUM.enable( uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(), // For SwiftUI or mixed apps: Both UIKit AND SwiftUI predicates needed swiftUIViewsPredicate: DefaultSwiftUIRUMViewsPredicate(), - swiftUIActionsPredicate: DefaultSwiftUIRUMActionsPredicate() + swiftUIActionsPredicate: DefaultSwiftUIRUMActionsPredicate(isLegacyDetectionEnabled: true) ) ) @@ -84,8 +90,13 @@ SessionReplay.enable( // Feature flags for experimental features // Default: [.swiftui: false] + // Available flags: + // .swiftui - Enable SwiftUI recording (experimental) + // .heatmaps - Enable heatmap identifier computation (experimental) + // .screenChangeScheduling - DEPRECATED: now default and always enabled; setting it has no effect featureFlags: [ - .swiftui: true // Enable SwiftUI recording (experimental) + .swiftui: true, // Enable SwiftUI recording (experimental) + .heatmaps: false // Enable heatmap identifier computation (experimental) ] ) ) @@ -218,6 +229,11 @@ SessionReplayPrivacyView( 1. Enable SwiftUI feature flag: `featureFlags: [.swiftui: true]` 2. Note: Session Replay SwiftUI is experimental, and some components are not supported +### Available feature flags +- `.swiftui` — Enable SwiftUI recording (experimental, default: `false`) +- `.heatmaps` — Enable heatmap identifier computation (experimental, default: `false`) +- `.screenChangeScheduling` — **Deprecated.** Screen change scheduling is now the default and always enabled; setting this flag has no effect. Kept on the public API for backward compatibility. + ## Feature Interactions - **RUM**: Required - Session Replay cannot work without RUM enabled. View and action tracking must be configured. diff --git a/Makefile b/Makefile index c9312fd54e..1b96085d97 100644 --- a/Makefile +++ b/Makefile @@ -424,6 +424,11 @@ api-surface-verify: --output-file /tmp/api-surface-objc-generated \ ../../api-surface-objc +# Verify feature doc files are up to date +feature-docs-verify: + @$(ECHO_TITLE) "make feature-docs-verify" + @./tools/feature-docs-verify.sh + # Builds API documentation using the same process as Swift Package Index. spi-docs-build: @$(ECHO_TITLE) "make spi-docs-build" diff --git a/docs/LLM_FEATURE_DOCS_GUIDELINES.md b/docs/LLM_FEATURE_DOCS_GUIDELINES.md index 070c7682b9..5ea40251f6 100644 --- a/docs/LLM_FEATURE_DOCS_GUIDELINES.md +++ b/docs/LLM_FEATURE_DOCS_GUIDELINES.md @@ -10,6 +10,16 @@ These files serve as **LLM-optimized entry points** to the codebase. They are NO - Highlight troubleshooting patterns - Show feature interactions and dependencies +## How updates flow + +Three pieces work together to keep these docs honest: + +- **`/dd-sdk-ios:update-feature-docs`** (Claude Code skill) — the canonical way to refresh feature docs. It discovers every `*_FEATURE.md`, audits `tracked_files` coverage against the doc's "Key Files" section, diffs against the last verified commit, updates the content where needed, and keeps the registries (the Confluence publish workflow + `AGENTS.md` + this file) in sync. +- **`tools/feature-docs-verify.sh`** (`make feature-docs-verify`) — drift detection. Runs in a `Feature Docs Verify` GitLab CI job on `release/*` and `hotfix/*` branches. Fails with a clear "run the skill" recipe when any doc has drifted from its tracked source files or any registry is missing an entry. +- **`.github/workflows/changelog-to-confluence.yaml`** — publishes the docs to the Confluence space used by Technical Escalation Engineers. + +For the full system overview (rationale, architecture, day-to-day workflow), see the *Feature Docs System* page in Confluence. + ## Feature Documentation Files Each feature module contains a `*_FEATURE.md` file at its root: @@ -26,21 +36,25 @@ Each feature documentation file contains a **"Key Files"** section listing all r ## File Metadata -Each feature documentation file should include a metadata header at the top: +Each feature documentation file must include a YAML frontmatter header at the top: ```markdown --- last_updated: YYYY-MM-DD sdk_version: X.Y.Z verified_against_commit: +tracked_files: + - Module/Sources/PublicAPI.swift + - Module/Sources/AnotherPublicAPI.swift --- ``` -- **last_updated**: Date when the file was last reviewed/updated -- **sdk_version**: SDK version the documentation was verified against -- **verified_against_commit**: Git commit hash of the source files used for verification +- **last_updated**: Date when the file was last reviewed/updated. +- **sdk_version**: SDK version the documentation was verified against. +- **verified_against_commit**: Short git commit hash that **must be reachable from `develop`**. The verify script uses `git diff ..HEAD` to detect drift in the tracked files. Don't use a PR-branch SHA — it can be orphaned by local rebases/amends or by certain merge strategies. Use `git merge-base origin/develop HEAD` to compute a stable value, or use the most recent develop commit your branch is built on. +- **tracked_files**: List of public-API source files whose changes should trigger a doc update. Every source file referenced in the doc's "Key Files" section must appear here — the verify script enforces this. -When updating, always update these metadata fields to reflect the current state. +When updating, always update these metadata fields to reflect the current state. The `/dd-sdk-ios:update-feature-docs` skill handles this automatically. ## Update Checklist diff --git a/tools/feature-docs-verify.sh b/tools/feature-docs-verify.sh new file mode 100755 index 0000000000..4e1e9892c3 --- /dev/null +++ b/tools/feature-docs-verify.sh @@ -0,0 +1,204 @@ +#!/bin/bash +# Verifies that feature docs are up to date with the public API. +# Discovers all *_FEATURE.md files in the repo. Each doc's frontmatter is the +# source of truth: `verified_against_commit` and `tracked_files` drive the check. +# Run `make feature-docs-verify` to execute. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +python3 - "$REPO_ROOT" <<'EOF' +import os +import subprocess +import sys + +repo_root = sys.argv[1] +failed = False + +def print_fix_instructions(full_clone=False): + """Print the standard recipe for fixing a feature-doc verification failure.""" + on_full_clone = " on a full clone" if full_clone else "" + print(f" Run the '/dd-sdk-ios:update-feature-docs' skill in Claude Code{on_full_clone} to refresh the doc,") + print(f" then `make feature-docs-verify` to confirm, and push the update.") + +def parse_frontmatter(path): + """Return (verified_against_commit, tracked_files) from a doc's YAML frontmatter.""" + commit = None + files = [] + in_front = False # True once we've seen the opening --- + in_files = False # True while we're inside the tracked_files list + + with open(path) as f: + for line in f: + line = line.rstrip("\n") + + # --- marks both the opening and closing of the frontmatter block + if line == "---": + if not in_front: + in_front = True + continue + else: + break # closing ---, stop parsing + + if not in_front: + continue + + if line.startswith("verified_against_commit:"): + commit = line.split(":", 1)[1].strip() + in_files = False + elif line.startswith("tracked_files:"): + in_files = True + elif in_files and line.startswith(" - "): + # Each list entry is " - path/to/file.swift" + files.append(line[4:].strip()) + elif line and not line.startswith(" "): + # Any non-indented key ends the tracked_files list + in_files = False + + return commit, files + +# Discover all *_FEATURE.md files, skipping generated output directories +docs = [] +for dirpath, dirnames, filenames in os.walk(repo_root): + dirnames[:] = [d for d in dirnames if d not in ("build", "artifacts")] + for name in filenames: + if name.endswith("_FEATURE.md"): + docs.append(os.path.join(dirpath, name)) + +if not docs: + print("No *_FEATURE.md files found.") + sys.exit(0) + +for doc in sorted(docs): + doc_name = os.path.basename(doc) + commit, files = parse_frontmatter(doc) + + if not commit: + print(f"❌ {doc_name}: missing verified_against_commit in frontmatter.") + print_fix_instructions() + failed = True + continue + + if not files: + print(f"❌ {doc_name}: missing tracked_files in frontmatter.") + print_fix_instructions() + failed = True + continue + + # Validate each tracked path actually exists. `git diff ..HEAD -- ` + # exits 0 with empty output, so without this check a typo or stale renamed + # path would silently report the doc as up to date. + missing = [f for f in files if not os.path.isfile(os.path.join(repo_root, f))] + if missing: + print(f"❌ {doc_name}: tracked_files references paths that don't exist in the working tree:") + for m in missing: + print(f" - {m}") + print(f" The file was likely renamed, moved, or deleted. Update the frontmatter") + print(f" to point at the current path(s).") + print_fix_instructions() + failed = True + continue + + # Check whether any tracked file changed since the doc was last verified + print(f"Checking {doc_name} against:") + for f in files: + print(f" - {f}") + abs_files = [os.path.join(repo_root, f) for f in files] + result = subprocess.run( + ["git", "-C", repo_root, "diff", f"{commit}..HEAD", "--", *abs_files], + capture_output=True, text=True + ) + + # `git diff` exits non-zero when the baseline commit isn't reachable + # (e.g. shallow clone on a release/hotfix branch, or a typo in the + # frontmatter). In that case stdout is empty, so we'd otherwise silently + # report the doc as up to date. Treat any non-zero exit as a failure and + # surface stderr so the engineer knows what to do. + if result.returncode != 0: + print(f"❌ {doc_name}: failed to diff against {commit}.") + stderr = result.stderr.strip() + if stderr: + for stderr_line in stderr.splitlines(): + print(f" {stderr_line}") + print(f" The baseline commit is not reachable in this checkout (often a shallow clone on a") + print(f" release/hotfix branch, or a typo in the frontmatter).") + print_fix_instructions(full_clone=True) + failed = True + elif result.stdout.strip(): + print(f"❌ {doc_name} may be out of date.") + print(f" Public API changed since commit {commit}.") + print_fix_instructions() + failed = True + else: + print(f"✅ {doc_name} is up to date (verified at {commit}).") + +# Each *_FEATURE.md must be wired into a few hand-maintained registries: +# - the Confluence publish workflow +# - AGENTS.md +# - docs/LLM_FEATURE_DOCS_GUIDELINES.md +# The Confluence workflow needs the path in TWO distinct places (the trigger +# `paths:` filter AND the `cp` block), so it gets a dedicated check that +# verifies both contexts separately. Other registries just need any mention. + +WORKFLOW_PATH = ".github/workflows/changelog-to-confluence.yaml" +WORKFLOW_LABEL = "Confluence publish workflow" +SUBSTRING_REGISTRIES = [ + ("AGENTS.md", "AGENTS.md"), + ("docs/LLM_FEATURE_DOCS_GUIDELINES.md", "LLM feature-docs guidelines"), +] + +doc_rel_paths = sorted(os.path.relpath(d, repo_root) for d in docs) +registry_failed = False + +print() +print("Checking that each feature doc is registered in:") +print(f" - {WORKFLOW_PATH} (paths: filter AND cp block)") +for path, _ in SUBSTRING_REGISTRIES: + print(f" - {path}") + +# --- Workflow check: two contexts must each contain the path --- +abs_workflow = os.path.join(repo_root, WORKFLOW_PATH) +if not os.path.exists(abs_workflow): + print(f"⚠️ {WORKFLOW_LABEL}: file not found at {WORKFLOW_PATH} — skipping.") +else: + with open(abs_workflow) as f: + workflow_content = f.read() + for rel in doc_rel_paths: + # `paths:` filter entries appear as quoted YAML list items. + in_paths_filter = (f"'{rel}'" in workflow_content) or (f'"{rel}"' in workflow_content) + # `cp` lines start with `cp ...`. + in_cp_block = f"cp {rel} " in workflow_content + + if not in_paths_filter and not in_cp_block: + print(f"❌ {WORKFLOW_LABEL}: '{rel}' is missing from BOTH the `paths:` filter and the `cp` block.") + registry_failed = True + elif not in_paths_filter: + print(f"❌ {WORKFLOW_LABEL}: '{rel}' is missing from the `paths:` filter") + print(f" (expected as `'{rel}'` under `on.push.paths`). Without it, doc-only updates won't trigger publishing.") + registry_failed = True + elif not in_cp_block: + print(f"❌ {WORKFLOW_LABEL}: '{rel}' is missing from the `cp` block") + print(f" (expected as `cp {rel} publish_folder/...`). Without it, the page is never copied for publishing.") + registry_failed = True + +# --- Substring registries (AGENTS.md, LLM guidelines): any mention is enough --- +for registry_path, label in SUBSTRING_REGISTRIES: + abs_path = os.path.join(repo_root, registry_path) + if not os.path.exists(abs_path): + print(f"⚠️ {label}: file not found at {registry_path} — skipping.") + continue + with open(abs_path) as f: + content = f.read() + for rel in doc_rel_paths: + if rel not in content: + print(f"❌ {label}: missing reference to '{rel}'.") + registry_failed = True + +if registry_failed: + print_fix_instructions() + failed = True +else: + print(f"✅ All {len(doc_rel_paths)} feature doc(s) registered in every required location.") + +sys.exit(1 if failed else 0) +EOF