diff --git a/plugins/aem/cloud-service/skills/migration/SKILL.md b/plugins/aem/cloud-service/skills/migration/SKILL.md index f16d869a..6c9ec254 100644 --- a/plugins/aem/cloud-service/skills/migration/SKILL.md +++ b/plugins/aem/cloud-service/skills/migration/SKILL.md @@ -7,9 +7,10 @@ description: | blockers: scheduler, ResourceChangeListener, replication, EventListener, OSGi EventHandler, DAM AssetManager, HTL data-sly-test lint, Classic UI / ExtJS / Coral 2 → Coral 3 dialog migration (lui), Custom Design Widgets (cdw), Vault package install-time dependencies (vault-package-dependencies), - Guava cache → Caffeine swaps (guavaCache), and static→editable template modernization. Also - externalizes OSGi config secrets to Cloud Manager (scans ui.config/.cfg.json for $[secret:]/$[env:] - placeholders) and converts AMS/on-prem Dispatcher configs to AEMaaCS (Branch E). + Guava cache → Caffeine swaps (guavaCache), Oak index conversion (oakIndex, beta), and + static→editable template modernization. Also externalizes OSGi config secrets to Cloud + Manager (scans ui.config/.cfg.json for $[secret:]/$[env:] placeholders) and converts + AMS/on-prem Dispatcher configs to AEMaaCS (Branch E). license: Apache-2.0 --- @@ -34,6 +35,7 @@ This skill drives the **migration workflow**: BPA data, CAM/MCP, **one pattern p | **OSGi → Cloud Manager** | *"**Scan my config files and create Cloud Manager environment secrets or variables.**"* | Agent **auto-reads** [references/osgi-cfg-json-cloud-manager.md](references/osgi-cfg-json-cloud-manager.md) (full Adobe-aligned rules inlined there); no BPA pattern id | | **HTL lint warnings** | *"Fix **htlLint** issues in `ui.apps`"* | Proactive discovery via `rg` → fix per the HTL lint reference | | **Vault package dependencies** | *"Fix **vault-package-dependencies** findings"* / *"Package install fails on AEMaaCS."* | Agent reads [references/vault-package-dependencies.md](references/vault-package-dependencies.md) — heuristic `pom.xml` scan (no BPA subtype, no analyzer) for `day/cq60/product:*` install-time deps in `content-package-maven-plugin`; removes the whole `` block. Not a `code-assessment` pattern — this dependency shape never occurs in native AEMaaCS code. | +| **Oak index findings (OID)** [BETA] | *"Fix **oakIndex** findings using `./path/to/bpa.csv`"* — covers `index.rule.violation` and `standard.index.modification` | Invokes Adobe's `@adobe/aem-cs-source-migration-index-converter` per [references/oak-index.md](references/oak-index.md); computes the next available `-custom-N` instead of assuming `-custom-1`; shows diff + type-compatibility check in IDE; validates with `mvn` and `aemanalyser`. **Branch F.** | | **Template modernization** | *"**Migrate my static templates to editable templates and generate Modernize Tools rules.**"* / *"Create editable templates from my static templates."* / *"Generate AEM Modernize Tools structure/component/policy rules."* | Agent **auto-reads** [references/template-modernization/template-modernization-context.md](references/template-modernization/template-modernization-context.md) (shared discovery + structured context), produces a **per-template plan table**, then executes the plan using [editable-template-creation.md](references/template-modernization/editable-template-creation.md) and [aem-modernization.md](references/template-modernization/aem-modernization.md), and validates via [template-modernization-validation.md](references/template-modernization/template-modernization-validation.md). No BPA pattern id. | | **Dialog migration** | *"Convert my Classic UI / ExtJS dialogs to Touch UI."* / *"Upgrade Coral 2 dialogs to Coral 3."* / *"Fix LUI dialog findings."* | Agent reads [references/legacy-ui/dialog/context.md](references/legacy-ui/dialog/context.md) — filters BPA LUI to dialog sub-types, converts via [extjs-to-coral3.md](references/legacy-ui/dialog/extjs-to-coral3.md) or [coral2-to-coral3.md](references/legacy-ui/dialog/coral2-to-coral3.md), validates via [validation.md](references/legacy-ui/dialog/validation.md). BPA pattern id: `lui`. | | **Custom widget migration** | *"Fix my CDW findings."* / *"Migrate custom ExtJS widgets to Coral 3."* | Agent reads [references/legacy-ui/cdw/context.md](references/legacy-ui/cdw/context.md) — inventories xtypes, maps or scaffolds Granite UI components via [conversion.md](references/legacy-ui/cdw/conversion.md), validates via [validation.md](references/legacy-ui/cdw/validation.md). BPA pattern id: `cdw`. Run CDW before dialog migration when both are needed. | @@ -46,6 +48,7 @@ This skill drives the **migration workflow**: BPA data, CAM/MCP, **one pattern p - *"Use the migration skill: **scheduler** only, BPA CSV at `./reports/bpa.csv`."* - *"**Replication** only from CAM; list projects first, I'll pick one."* - *"**Manual:** **event listener** migration for `.../Listener.java`."* +- *"Fix **oakIndex** findings using BPA CSV at `./reports/bpa.csv`."* - *"Fix all Classic UI and custom widget findings — CDW first, then dialogs."* - *"Fix **guavaCache** findings using BPA CSV at `./reports/bpa.csv`."* @@ -118,6 +121,19 @@ If the user asks to **convert / migrate a Dispatcher configuration** to AEM as a 5. **Judgment + cross-boundary** — apply the decision catalog in [references/dispatcher/conversion-patterns.md](references/dispatcher/conversion-patterns.md); hand any Cloud Manager environment variables to **Branch A** (OSGi → Cloud Manager); flag CDN-candidate rules. Target end-state conventions are in [references/dispatcher/current-sdk-conventions.md](references/dispatcher/current-sdk-conventions.md). To operationalize this handoff, run `scripts/dispatcher-crossboundary.js` to build the Cloud Manager variable handoff artifact, then apply [references/dispatcher/cross-boundary.md](references/dispatcher/cross-boundary.md) to route each concern — CM vars → **Branch A**; immutable freshness → the `dispatcher` skill's `sdk(diff-baseline)`; security headers / edge → `security-hardening`; validation → `config-authoring`. 6. **Validate** — validate the converted `src` per [references/dispatcher/validation.md](references/dispatcher/validation.md) (delegates to the `dispatcher` skill's SDK validator + guardrails); iterate until clean. Do not present the result as done on validation failure. After validation, render the consolidated report with `scripts/dispatcher-report.js` (`renderReport` → `writeReport`) as `conversion-report.md`, which includes the coverage counts, the CM handoff, and the delegated next-checks checklist. +**Branch F — Oak Index Migration** (legacy `_oak_index/*.xml` → AEM as a Cloud Service compatible Oak index definitions; BPA pattern id `oakIndex`, category OID): + +> **Beta**: Oak index migration (Branch F) is in beta and under active development. Review its output carefully — especially the type/datatype compatibility check in step 4 — before using it on production index definitions. + +If the user asks to **fix Oak index findings / convert `_oak_index` definitions** to AEM as a Cloud Service, follow the **6-step flow** in [references/oak-index.md](references/oak-index.md). It wraps Adobe's maintained `@adobe/aem-cs-source-migration-index-converter` as the conversion engine and adds detection, a dynamic `-custom-N` naming override, a type-compatibility check, and validation on top. **Skip** Branch B. + +1. **Detect** — locate `_oak_index/` directories; determine `aemVersion` (`63`/`64`/`65`/`Cloud_Services`) from the project's `pom.xml`. +2. **Determine next `-custom-N`** — scan for existing `-custom-` siblings already in the target state before assuming the tool's default `-custom-1`; a prior migration or a customer's own `-custom-N` index makes reusing `-custom-1` a collision, not a fresh name. +3. **Execute** — resolve the Index Converter via `npx` and invoke it per [references/oak-index.md](references/oak-index.md#step-3--invoke-index-converter). +4. **Diff + type-compatibility check** — show the converted output vs. input; verify `oak:isOakIndexDefinition`/`type` are intact and that per-property JCR value types match between legacy and converted definitions. Flag any mismatch as `needs_manual_review: datatype mismatch on ` — do not silently apply it. +5. **Apply** (after user confirms) — copy tool output into the repo, applying the Step 2 name override if one was computed. +6. **Validate** — `mvn -pl ui.apps clean install`, then `mvn -pl all aem-analyser:project-analyse` if configured. Report PASS/FAIL with file:line evidence. + ## When to Use This Skill - Migrate legacy AEM Java toward **Cloud Service–compatible** patterns (scheduler, ResourceChangeListener, replication, EventListener/EventHandler, AssetManager) @@ -127,6 +143,7 @@ If the user asks to **convert / migrate a Dispatcher configuration** to AEM as a - **OSGi → Cloud Manager** secret/variable externalization (Branch A), **Template Modernization** (Branch C), **Legacy UI** dialog/CDW migration (Branch D) - Drive work from **BPA** (CSV or cached collection) or **CAM via MCP**, **one pattern per session** - **Dispatcher Conversion:** **Branch E** — convert AMS / on-premise Apache+Dispatcher configs to AEMaaCS via Adobe's `dispatcher-converter`, with mode detection, config generation, output verification (filter/ACL hard-gate), cross-boundary handoff, and SDK validation. References: [references/dispatcher/](references/dispatcher/). +- **Oak Index Migration:** **Branch F** (beta) — convert legacy `_oak_index/*.xml` definitions via Adobe's `index-converter`, with a dynamic `-custom-N` naming override, a type/datatype compatibility check, and validation. BPA pattern id: `oakIndex`. Reference: [references/oak-index.md](references/oak-index.md). Branch routing and the read-first delegation for each entry above are defined once in **Required delegation** — this list is only the "when." @@ -249,8 +266,9 @@ The runbook covers **every pattern the migration skill can address**. Each patte | `lui`, `cdw`, `templateModernization` | BPA `cascade` → `content-scan` fallback | When a BPA CSV/CAM source is present, these come from BPA (subtypes `custom.classic.widget`; `legacy.dialog.classic`/`.coral2`; `legacy.static.template` + `custom.static.template`). With no BPA source, a heuristic `.content.xml` scan is the fallback — for `templateModernization` it walks `apps//templates/**` at **any depth** (nested/grouped templates included) and classifies each static template as `custom.static.template` or `legacy.static.template` from its page-component resource type, so the custom-vs-legacy distinction survives even without a BPA report. Sample prompts route to **Branch D** (legacy-ui) / **Branch C** (templates), not code-assessment | | `guavaCache` | `bpa-only` (no analyzer, no content-scan) | BPA is the **sole** source of truth (subtype `custom.guava.cache`), one finding per **bundle** — `identifier` on this subtype is a Guava-internal class, not a customer class, so raw rows are deduped to the bundle named in the message, not surfaced per row. With no BPA source, `guavaCache` has no deterministic fallback and surfaces under **Tier 4 — LLM scan**: the agent greps `.java` files for `import com.google.common.cache` per module, per [references/guava-cache.md](references/guava-cache.md), and tags the result `confidence: llm`. There is deliberately no compiled analyzer detector for this pattern — it does not run inside `code-assessment`'s own discovery. | | `dispatcherConversion` | `content-scan` | Heuristic scan for an AMS / on-prem Dispatcher config layout (a monolithic `dispatcher.any` + `conf.vhost.d/`, or `conf.dispatcher.d/` AMS trees). Detected by `dispatcher-inventory.js`; the sample prompt routes to **Branch E**. | +| `oakIndex` | `bpa-only` (no analyzer, no content-scan) | BPA is the **sole** source of truth (subtypes `index.rule.violation` / `standard.index.modification`), one finding per Oak index path. With no BPA source, `oakIndex` has no deterministic fallback and surfaces under **Tier 4 — LLM scan**: the agent locates `_oak_index/*.xml` directly per [references/oak-index.md](references/oak-index.md). Sample prompt routes to **Branch F** (beta). | -`htlLint`, `osgiConfig`, `vault-package-dependencies`, and the content-scan **fallback** for `lui`/`cdw`/`templateModernization` are **heuristic** (tagged `confidence: heuristic` in the cache) — candidate matches, not compiler-validated. BPA-sourced `lui`/`cdw`/`templateModernization`/`replication`/`guavaCache` findings are authoritative. Out of scope: `inject-in-sling-model` and `outdated-dependencies` (those belong to code-assessment's own runbook, not migration). +`htlLint`, `osgiConfig`, `vault-package-dependencies`, and the content-scan **fallback** for `lui`/`cdw`/`templateModernization` are **heuristic** (tagged `confidence: heuristic` in the cache) — candidate matches, not compiler-validated. BPA-sourced `lui`/`cdw`/`templateModernization`/`replication`/`guavaCache`/`oakIndex` findings are authoritative. Out of scope: `inject-in-sling-model` and `outdated-dependencies` (those belong to code-assessment's own runbook, not migration). **BPA is the source of truth when a report is available.** `lui`/`cdw`/`templateModernization`/`replication` are read from the BPA CSV/CAM (the parser now extracts these subtypes and excludes `_COUNT_*`/`_STAT` summary rows), so the runbook counts match your BPA report's LUI-dialog / CDW / static-template / REP tallies. `lui` keeps only the dialog sub-types (`legacy.custom.component` → create-component; `legacy.static.template` is counted under `templateModernization`). The `.content.xml` scan is only the fallback when no BPA source is present — and it can **undercount** relative to BPA when the flagged legacy nodes live in packages (e.g. acs-commons) not in the project source. `replication`: BPA `replication.agent` findings when a report is present, else the analyzer detects `Replicator` usage from source. @@ -307,6 +325,8 @@ First check the non-Java branches (routed in full under **Required delegation**) If the request is **dispatcher conversion** — convert or migrate an AMS or on-premise Dispatcher configuration to AEM as a Cloud Service — follow **Branch E**. No Java pattern module is needed. **Skip** Branch B. +If the request is **oak index migration** — fix `oakIndex` findings, or convert `_oak_index` definitions to AEM as a Cloud Service — follow **Branch F** (beta). No Java pattern module is needed. **Skip** Branch B. + Otherwise map the request to a pattern id: `scheduler`, `resourceChangeListener`, `replication`, `eventListener`, `eventHandler`, `assetApi`, `htlLint`, `lui`, `cdw`. If unclear, use **Manual Pattern Hints** in **`{code-assessment}/SKILL.md`** or ask the user to pick one of those. ### Step 2: Availability diff --git a/plugins/aem/cloud-service/skills/migration/references/oak-index.md b/plugins/aem/cloud-service/skills/migration/references/oak-index.md new file mode 100644 index 00000000..6243e21b --- /dev/null +++ b/plugins/aem/cloud-service/skills/migration/references/oak-index.md @@ -0,0 +1,134 @@ +# Oak Index Migration Pattern + +> **Beta Skill**: This skill is in beta and under active development. +> Results should be reviewed carefully before use in production. +> Report issues at https://github.com/adobe/skills/issues + +Rewrites legacy `_oak_index/*.xml` definitions to AEM as a Cloud Service compatible Oak index definitions by invoking Adobe's official **`@adobe/aem-cs-source-migration-index-converter`** CLI tool. Covers BPA subtypes `index.rule.violation` and `standard.index.modification` (category **OID**). + +Not a Cloud-Service-native code-quality issue — legacy `_oak_index/*.xml` layouts only exist in pre-cloud (AEM 6.x / AMS) projects, so this is a migration reference, not a `code-assessment` pattern. Like Dispatcher Conversion (Branch E), this pattern wraps an Adobe-maintained conversion tool rather than re-implementing transformation rules, so it lives under `migration/references/` only. + +**Before transformation steps:** [aem-cloud-service-pattern-prerequisites.md](aem-cloud-service-pattern-prerequisites.md). + +**Scope:** +- Custom Oak index definitions under `ui.apps/.../_oak_index/` +- OOTB index modifications (e.g. `damAssetLucene` customized in place) +- Lucene type indexes; property/ordered indexes are passed through unchanged by the tool + +**Out of scope (skill stops, agent reports to user):** +- Indexes outside `_oak_index/` (e.g. JSON definitions deployed at runtime) +- `nt:base` lucene indexes (the tool refuses to convert these) + +## How the skill runs + +The skill does **not** re-implement transformation rules. It invokes the Adobe-maintained tool, captures the output, shows the diff, and validates. + +### Step 1 — Detect + +- Locate `_oak_index/` directories under `ui.apps/src/main/content/jcr_root/`. If none exist, stop and report to user. +- Determine `aemVersion` for the config: the tool maps this value directly to a bundled baseline file (`.content_.xml`). Valid values are `63`, `64`, `65`, and `Cloud_Services`. Use `Cloud_Services` for AEM as a Cloud Service / SDK projects (i.e. when `pom.xml` contains `aem.sdk.api` or `aem.sdk.api.version`). Use `65` for AEM 6.5, `64` for AEM 6.4, `63` for AEM 6.3. + +### Step 2 — Determine the next custom-N (do not assume `-custom-1`) + +The tool's own default output is always `-custom-1` for a first-time conversion. **Do not trust that blindly** — if this repo has already been through a prior Oak index migration (or a customer manually added `-custom-N` indexes since), reusing `-custom-1` will collide with, or silently shadow, an existing index. + +Before invoking the tool: +1. Scan the **current target state** for existing `-custom-` siblings — check both `ui.apps/.../_oak_index/.content.xml` (already-migrated definitions committed by a previous run) and, if available, the target Cloud Service environment's live index list. +2. For each index the tool will rename, compute `n = highest existing -custom- suffix found (0 if none)`, then use `-custom-` as the **actual** name applied in Step 4 — overriding the tool's raw `-custom-1` output for that index if the computed `n+1` differs. +3. Record the mapping (`tool output name` → `actual applied name`) so Step 3's diff and Step 5's validation both refer to the corrected name, not the tool's raw one. + +If no prior `-custom-N` index exists for a given base name, the tool's own `-custom-1` output is correct as-is — no override needed. + +### Step 3 — Invoke Index Converter + +The package has no `bin` entry, so it can't be run as a plain `npx ` command — it must be `require()`'d as a script, which executes it (it has no `require.main` guard) and reads `config.yaml` from `process.cwd()`. Use `npx -y` to resolve/cache the package (no manual `npm install` or throwaway `node_modules`) and only create a small directory for the required `config.yaml`: + +```bash +# 1. config.yaml must live in cwd when the executor runs +WORK_DIR="/tmp/oak-index-tool-" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" +cat > config.yaml << 'YAML' +indexConverter: + ensureIndexDefinitionContentPackageJcrRootPath: + ensureIndexDefinitionConfigPackageJcrRootPath: + aemVersion: Cloud_Services + customOakIndexDirectoryPath: /ui.apps/src/main/content/jcr_root/_oak_index + filterXMLPath: /ui.apps/src/main/content/META-INF/vault/filter.xml +YAML + +# 2. Resolve the npx-cached install (npx handles install + caching for future runs) +NODE_PATH="$(npx -y -p @adobe/aem-cs-source-migration-index-converter -c 'echo "${PATH%%:*}"')" +export NODE_PATH="${NODE_PATH%/.bin}" + +# 3. Run the executor (require() triggers it; it reads config.yaml from cwd above) +node -e "require('@adobe/aem-cs-source-migration-index-converter/executors/index-converter.js')" +``` + +The tool writes output to `./target/index/` under the working directory: +- `./target/index/.content.xml` — the converted oak index XML +- `./target/index/filter.xml` — updated filter.xml with renamed index paths +- `./target/index/index-converter-report.md` — conversion report + +It does **not** modify the input. + +### Step 4 — Show diff in IDE + +Diff the input vs. tool output, **using the corrected name from Step 2** wherever it overrides the tool's raw `-custom-1`: + +```bash +diff /ui.apps/src/main/content/jcr_root/_oak_index/.content.xml \ + $WORK_DIR/target/index/.content.xml +diff /ui.apps/src/main/content/META-INF/vault/filter.xml \ + $WORK_DIR/target/index/filter.xml +``` + +Also show `$WORK_DIR/target/index/index-converter-report.md` — it lists which indexes were converted and which need manual migration. + +**Type/datatype compatibility check (do this before presenting the diff as done):** for each converted index, confirm: +- The output node's `jcr:primaryType` is `oak:QueryIndexDefinition` and it declares `oak:isOakIndexDefinition="{Boolean}true"` and `type="lucene"` (or the applicable index type) — the tool should always produce this, but a mismatch here means the conversion silently failed for that node and must be flagged, not applied. +- Every property present in **both** the legacy and converted definitions (e.g. `propertyIndex`, individual `properties/*/name` and `properties/*/type` nodes) carries the **same JCR value type** — an existing `String` property re-declared as `Long` (or vice versa) in the converted output is an incompatibility the tool report may not surface explicitly. Cross-check property `type` attributes between old and new by name. +- If a datatype mismatch is found, **do not silently apply the tool's output for that property** — flag it in the diff summary shown to the user as `needs_manual_review: datatype mismatch on ` and let the tool's report guidance (or a project domain expert) resolve it before Step 5. + +### Step 5 — Apply (after user confirms) + +If the user accepts: +```bash +cp $WORK_DIR/target/index/.content.xml \ + /ui.apps/src/main/content/jcr_root/_oak_index/.content.xml +cp $WORK_DIR/target/index/filter.xml \ + /ui.apps/src/main/content/META-INF/vault/filter.xml +``` +If Step 2 computed a corrected name, apply the rename in the copied `.content.xml`/`filter.xml` before staging (the node name and any `filter.xml` path referencing the tool's raw `-custom-1` output must both be updated to the corrected `-custom-` name). +- Stage for commit + +### Step 6 — Validate + +Run validation in this order, gate on each: + +```bash +# Compile (catches XML / filter.xml errors) +mvn -pl ui.apps clean install + +# Cloud-readiness analyser (if pom has aemanalyser-maven-plugin) +mvn -pl all aem-analyser:project-analyse +``` + +Report PASS or FAIL with file:line evidence on FAIL. + +## Naming conventions produced by the tool + +The Index Converter applies these naming rules (these are the tool's behavior, documented here for reference; the skill does **not** re-implement them — but does **override** the suffix per Step 2 when a higher `-custom-N` already exists): + +- **OOTB extension:** `--custom-1` (e.g. `damAssetStateIndex-3-custom-1`) — the OOTB name and version come from the bundled Cloud Services baseline XML for the configured `aemVersion`, not from the legacy index's own name. +- **New custom index:** `-custom-1` (e.g. `wkndId-custom-1`) +- **Already conforming:** passed through unchanged + +**Roadmap note:** [Simplified Index Management](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/operations/indexing) is Adobe's newer index-authoring model. This pattern currently targets the Index Converter tool's classic `_oak_index/*.xml` output; adopting Simplified Index Management as the target format is a separate, larger change tracked outside this reference — Step 2's dynamic custom-N logic is written to be forward-compatible with either target once that decision is made. + +## What the skill does NOT do + +- Does not rewrite XML by hand using rules encoded in this file +- Does not decide whether to use Lucene vs Elasticsearch +- Does not modify queries that depend on the renamed indexes (separate task) +- Does not deploy to a running AEM instance diff --git a/plugins/aem/cloud-service/skills/migration/scripts/bpa-local-parser.js b/plugins/aem/cloud-service/skills/migration/scripts/bpa-local-parser.js index 4b09a422..f6d14262 100644 --- a/plugins/aem/cloud-service/skills/migration/scripts/bpa-local-parser.js +++ b/plugins/aem/cloud-service/skills/migration/scripts/bpa-local-parser.js @@ -21,6 +21,7 @@ const PATTERN_TO_SUBTYPE = { scheduler: "sling.commons.scheduler", assetApi: "unsupported.asset.api", guavaCache: "custom.guava.cache", + oakIndex: "oak.index.definition", }; // CSV subtype to pattern mapping (based on actual CSV structure) @@ -29,7 +30,9 @@ const CSV_SUBTYPE_TO_PATTERN = { "javax.jcr.observation.EventListener": "eventListener", "org.apache.sling.api.resource.observation.ResourceChangeListener": "resourceChangeListener", "org.osgi.service.event.EventHandler": "eventHandler", - "custom.guava.cache": "guavaCache" + "custom.guava.cache": "guavaCache", + "index.rule.violation": "oakIndex", + "standard.index.modification": "oakIndex" }; // Known scheduler identifier @@ -479,6 +482,44 @@ function processGuavaCacheFindings(findings) { }; } +/** + * Process oak index findings from CSV. `identifier` is the oak index JCR path + * (e.g. `/content/oak:index/enablementResourceName`), not a class name — kept + * as raw array values (never used as an object key), so no mongo-safe + * round-trip risk. One real index path produces multiple raw CSV rows (one + * per distinct rule violation — "must follow -custom-N pattern", "path must + * begin with /oak:/index", "type must be lucene", etc., all for the same + * path) — dedupe by path per subtype, not by row. Both BPA subtypes for + * category OID (`index.rule.violation`, `standard.index.modification`) are + * emitted under the single `oakIndex` pattern, keyed separately since a path + * can independently need both a naming fix and a modification review. + */ +function processOakIndexFindings(findings) { + const oakIndexFindings = findings.filter(finding => + (finding.subtype === 'index.rule.violation' || finding.subtype === 'standard.index.modification') && + !String(finding.code || '').startsWith('_') + ); + + const identifiers = {}; + + oakIndexFindings.forEach(finding => { + const subtype = finding.subtype; + const indexPath = (finding.identifier || '').trim(); + if (!indexPath) return; + if (!identifiers[subtype]) { + identifiers[subtype] = []; + } + if (!identifiers[subtype].includes(indexPath)) { + identifiers[subtype].push(indexPath); + } + }); + + return { + subtype: 'oak.index.definition', + identifiers: identifiers + }; +} + /** * Convert subtype to MongoDB-safe field name (matching cloud-adoption-service) */ @@ -603,6 +644,21 @@ function createUnifiedCollection(bpaData, outputDir) { console.log(`Found ${Object.values(guavaCacheCollection.identifiers).flat().length} bundles using Guava cache`); } + // Process oak index findings + const oakIndexCollection = processOakIndexFindings(findings); + if (Object.keys(oakIndexCollection.identifiers).length > 0) { + const mongoSafeSubtype = toMongoSafeFieldName(oakIndexCollection.subtype); + subtypes[mongoSafeSubtype] = {}; + + Object.entries(oakIndexCollection.identifiers).forEach(([identifier, indexPaths]) => { + const mongoSafeIdentifier = toMongoSafeIdentifier(identifier); + subtypes[mongoSafeSubtype][mongoSafeIdentifier] = indexPaths; + totalFindings += indexPaths.length; + }); + + console.log(`Found ${Object.values(oakIndexCollection.identifiers).flat().length} oak index paths`); + } + // Process content / legacy-UI subtypes (cdw, lui, templates, replication). // Keys are RAW JCR paths (not MongoDB-safed) to avoid corrupting underscores. for (const subtype of CONTENT_SUBTYPES) { diff --git a/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.js b/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.js index c94737d1..c3a4a075 100644 --- a/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.js +++ b/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.js @@ -37,17 +37,24 @@ * dialogs, custom `cq:Widget` xtypes, static templates) is the * heuristic fallback when no BPA source is available. Routed to * migration Branches D / C, not code-assessment. - * 'bpa-only' — `guavaCache`: BPA/CAM/CSV only (subtype `custom.guava.cache`), - * one finding per bundle. `identifier` on this subtype is a + * 'bpa-only' — `guavaCache`, `oakIndex`: BPA/CAM/CSV only, no analyzer, no + * content-scan fallback. + * `guavaCache` (subtype `custom.guava.cache`): one finding + * per bundle. `identifier` on this subtype is a * Guava-internal class, not a customer class — BPA reports * every Guava-internal class reachable on a bundle's * classpath, so raw rows are deduped to the bundle named in - * the message, not surfaced per row. No analyzer, no - * content-scan — Guava cache usage does not occur in native - * AEMaaCS code, so there is deliberately no compiled - * detector for it. With no BPA source the pattern surfaces - * in `needsLlmScan` like any other - * unscanned pattern. + * the message, not surfaced per row. Guava cache usage does + * not occur in native AEMaaCS code, so there is + * deliberately no compiled detector for it. + * `oakIndex` (subtypes `index.rule.violation` / + * `standard.index.modification`, beta, Branch F): one + * finding per Oak index JCR path. Legacy `_oak_index/*.xml` + * layouts only exist in pre-cloud projects, so — like + * Dispatcher Conversion — this wraps Adobe's index-converter + * tool rather than a compiled detector. + * With no BPA source, either pattern surfaces in + * `needsLlmScan` like any other unscanned pattern. * * `html-scan`/`config-scan`/`content-scan` (fallback) findings are tagged `confidence: 'heuristic'` in the * cache. Patterns no available strategy could scan (e.g. a cascade pattern @@ -84,10 +91,11 @@ const { runDispatcherScan } = require('./dispatcher-inventory.js'); // 'config-scan' — config-file heuristic scan (osgiConfig) // 'content-scan' — .content.xml / template / dispatcher-config scan (lui, cdw, // templateModernization, dispatcherConversion) -// 'bpa-only' — BPA/CAM/CSV only, no local fallback (guavaCache) +// 'bpa-only' — BPA/CAM/CSV only, no local fallback (guavaCache, oakIndex) // `bpaSlugs` maps a pattern to its BPA subtype(s): the Java 'cascade' patterns, -// plus replication (replication.agent), lui/cdw/templateModernization, and -// guavaCache (com.google.common.cache). When a BPA source is present it is +// plus replication (replication.agent), lui/cdw/templateModernization, +// guavaCache (com.google.common.cache), and oakIndex (index.rule.violation / +// standard.index.modification). When a BPA source is present it is // authoritative; html/config/content scans are the local fallback for the // patterns that have one. (inject-in-sling-model and outdated-dependencies // belong to code-assessment's own runbook, not the migration runbook, so they @@ -234,6 +242,15 @@ const PATTERN_META = { promptPattern: 'dispatcher conversion', sampleOverride: 'Use the migration skill: convert my dispatcher configuration to AEM as a Cloud Service.', }, + oakIndex: { + label: 'Oak Index Migration', + severity: 'high', + strategy: 'bpa-only', + bpaSlugs: ['oakIndex'], + description: 'Legacy `_oak_index/*.xml` definitions (BPA subtypes `index.rule.violation` / `standard.index.modification`, category OID) convertible to AEM as a Cloud Service Oak index definitions (Branch F, beta). BPA is the sole source of truth — one finding per Oak index JCR path; there is no analyzer or content-scan fallback. Wraps Adobe\'s aem-cs-source-migration index-converter tool rather than re-implementing transformation rules.', + promptPattern: 'oakIndex', + sampleOverride: 'Use the migration skill: fix oakIndex findings using BPA CSV at ./reports/bpa.csv.', + }, }; // content-scan strategy → the runner that produces that pattern's findings. diff --git a/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.test.js b/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.test.js index 06169892..1f8b9b98 100644 --- a/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.test.js +++ b/plugins/aem/cloud-service/skills/migration/scripts/runbook-generator.test.js @@ -30,11 +30,11 @@ function write(root, rel, content) { // ── Pattern registry ──────────────────────────────────────────────────────── -test('registry includes all 13 migration patterns with a valid strategy', () => { +test('registry includes all migration patterns with a valid strategy', () => { const expected = [ 'scheduler', 'resourceChangeListener', 'event-migration', 'assetApi', 'replication', 'vault-package-dependencies', 'htlLint', 'osgiConfig', 'lui', 'cdw', 'templateModernization', - 'guavaCache', 'dispatcherConversion', + 'guavaCache', 'dispatcherConversion', 'oakIndex', ]; assert.strictEqual(CANONICAL_PATTERNS.length, expected.length, 'no unexpected patterns'); for (const key of expected) { @@ -54,6 +54,12 @@ test('guavaCache has no analyzer/content-scan fallback — bpaSlugs only, no heu assert.ok(!PATTERN_META.guavaCache.heuristic, 'guavaCache findings are BPA-authoritative, not heuristic'); }); +test('oakIndex has no analyzer/content-scan fallback — bpaSlugs only, no heuristic flag', () => { + assert.strictEqual(PATTERN_META.oakIndex.strategy, 'bpa-only'); + assert.deepStrictEqual(PATTERN_META.oakIndex.bpaSlugs, ['oakIndex']); + assert.ok(!PATTERN_META.oakIndex.heuristic, 'oakIndex findings are BPA-authoritative, not heuristic'); +}); + test('inject-in-sling-model and outdated-dependencies stay out of scope', () => { assert.ok(!CANONICAL_PATTERNS.includes('inject-in-sling-model')); assert.ok(!CANONICAL_PATTERNS.includes('outdated-dependencies')); @@ -999,6 +1005,50 @@ test('guavaCache dedupes multiple rows for one bundle to a single target, and wa ); }); +function writeOakIndexBpaCsv(root) { + const rows = [ + 'code,type,subtype,importance,identifier,message,context', + // Two rule-violation rows for the SAME index path — must dedupe to one target. + 'OID,oak.index.definition,index.rule.violation,CRITICAL,/content/oak:index/enablementResourceName,The /content/oak:index/enablementResourceName Oak index has a rule violation: Custom definition name must follow the pattern -custom-,ctx', + 'OID,oak.index.definition,index.rule.violation,CRITICAL,/content/oak:index/enablementResourceName,The /content/oak:index/enablementResourceName Oak index has a rule violation: Path must begin with /oak:/index,ctx', + // A distinct rule-violation path. + 'OID,oak.index.definition,index.rule.violation,CRITICAL,/oak:index/wkndId,The /oak:index/wkndId Oak index has a rule violation: Custom definition name must follow the pattern -custom-,ctx', + // A standard-index-modification finding (OOTB index modified in place). + 'OID,oak.index.definition,standard.index.modification,CRITICAL,/oak:index/damAssetLucene,The /oak:index/damAssetLucene Oak index modifies a standard OOTB index definition,ctx', + // Summary row — bare numeric identifier, must be excluded, not mistaken for a path. + '_COUNT_OID,_count.oak.index.definition,index.rule.violation,CRITICAL,182,Count: 182,ctx', + ]; + const p = path.join(root, 'oakindex.csv'); + fs.writeFileSync(p, rows.join('\n') + '\n', 'utf8'); + return p; +} + +test('oakIndex dedupes repeat rows for the same index path, excludes _COUNT_OID, and splits by sub-type', async () => { + const root = mkworkspace(); + const csv = writeOakIndexBpaCsv(root); + const opts = { bpaFilePath: csv, collectionsDir: path.join(root, 'uc'), limit: null, offset: 0 }; + + const result = await getBpaFindings('oakIndex', opts); + + assert.strictEqual(result.targets.length, 3, 'two rule-violation rows for the same path dedupe to one target, plus the distinct path and the standard-modification finding'); + assert.strictEqual( + result.targets.filter(t => t.identifier === 'index.rule.violation').length, 2, + 'two distinct rule-violation index paths' + ); + assert.strictEqual( + result.targets.filter(t => t.identifier === 'standard.index.modification').length, 1, + 'one standard-index-modification finding' + ); + assert.ok( + result.targets.some(t => t.className === '/content/oak:index/enablementResourceName'), + 'JCR index path preserved as the actionable className' + ); + assert.ok( + !result.targets.some(t => t.className === '182'), + '_COUNT_OID summary row (bare numeric identifier) must not be mistaken for an index path' + ); +}); + test('a real BPA fetch failure is surfaced (warning + needsLlmScan), NOT reported clean', async () => { const root = mkworkspace(); const gathered = await gatherFindings({ diff --git a/plugins/aem/cloud-service/skills/migration/scripts/unified-collection-reader.js b/plugins/aem/cloud-service/skills/migration/scripts/unified-collection-reader.js index 9cf1b7f6..1486282a 100644 --- a/plugins/aem/cloud-service/skills/migration/scripts/unified-collection-reader.js +++ b/plugins/aem/cloud-service/skills/migration/scripts/unified-collection-reader.js @@ -19,7 +19,8 @@ const PATTERN_TO_SUBTYPE = { eventListener: "javax.jcr.observation.EventListener", resourceChangeListener: "org.apache.sling.api.resource.observation.ResourceChangeListener", eventHandler: "org.osgi.service.event.EventHandler", - guavaCache: "custom.guava.cache" + guavaCache: "custom.guava.cache", + oakIndex: "oak.index.definition" }; // MongoDB-safe to pattern mapping @@ -29,7 +30,8 @@ const MONGO_SAFE_TO_PATTERN = { "javax_jcr_observation_EventListener": "eventListener", "org_apache_sling_api_resource_observation_ResourceChangeListener": "resourceChangeListener", "org_osgi_service_event_EventHandler": "eventHandler", - "custom_guava_cache": "guavaCache" + "custom_guava_cache": "guavaCache", + "oak_index_definition": "oakIndex" }; // Pattern → subtype(s), 1:many. Covers the Java patterns above plus the @@ -48,6 +50,7 @@ const PATTERN_TO_SUBTYPES = { replication: ["forward.replication", "reverse.replication"], urc: ["unsupported.runmode"], guavaCache: ["custom.guava.cache"], + oakIndex: ["oak.index.definition"], }; // Patterns whose findings are keyed by JCR path (raw keys, generic processor). @@ -363,6 +366,41 @@ function processGuavaCacheFromUnified(subtypeData, targets) { return count; } +/** + * Process Oak index data from unified collection. Unlike the content/LUI + * patterns, the unified data here is keyed by the BPA **sub-type** string + * (`index.rule.violation` / `standard.index.modification`, MongoDB-safe + * round-tripped), each mapping to an array of raw JCR index paths — so a + * generic `processContentFromUnified` pass (keyed by JCR path) doesn't fit. + * + * Field shape mirrors `processGuavaCacheFromUnified`: `className` carries the + * actionable JCR index path, and `identifier` carries the BPA sub-type string + * (which distinguishes a full rule violation from an OOTB index modified in + * place — both need the Index Converter tool, but the message differs). + */ +function processOakIndexFromUnified(subtypeData, targets) { + let count = 0; + + const identifierKeys = Object.keys(subtypeData || {}).sort(); + for (const mongoSafeIdentifier of identifierKeys) { + const indexPaths = subtypeData[mongoSafeIdentifier] || []; + const identifier = fromMongoSafeFieldName(mongoSafeIdentifier); + + for (const indexPath of indexPaths) { + count++; + targets.push(new BpaTarget( + "oakIndex", + indexPath, + identifier, + `Oak index definition (${identifier}): ${indexPath}`, + "high" + )); + } + } + + return count; +} + /** * Process a content/legacy-UI subtype whose unified data is keyed by RAW JCR * path (no MongoDB round-trip). Emits one target per finding, with the JCR path @@ -472,6 +510,7 @@ function fetchUnifiedBpaFindings(pattern = "all", collectionsDir = './unified-co resourceChangeListener: processResourceChangeListenerFromUnified, eventHandler: processEventHandlerFromUnified, guavaCache: processGuavaCacheFromUnified, + oakIndex: processOakIndexFromUnified, }; // Process each pattern — a pattern may map to more than one subtype.