fix: revert change to resolved.json $schema url (#2878)

Fix #2877

### Changes are visible to end-users: no

### Test plan

- Covered by existing test cases
- New test cases added
diff --git a/.github/workflows/ci-workflows.yaml b/.github/workflows/ci-workflows.yaml
index a42c615..289153c 100644
--- a/.github/workflows/ci-workflows.yaml
+++ b/.github/workflows/ci-workflows.yaml
@@ -89,6 +89,7 @@
                     e2e/protobuf-google
                     e2e/repo_mapping
                     e2e/output_paths
+                    e2e/ts_version_from_rules_ts_3.8.10
                     e2e/update_pnpm_lock
                     e2e/update_pnpm_lock_with_import
                     e2e/vendored_node
@@ -117,8 +118,8 @@
 
                   # Slug feeds aspect --task-key, the disk-cache key, and the job name, all
                   # of which require [A-Za-z0-9_-]. Map "." → "root"; replace "/" with "-"
-                  # and "+" with "_" so e.g. e2e/npm_translate_lock_git+ssh becomes a valid
-                  # e2e-npm_translate_lock_git_ssh slug.
+                  # and "+" and "." with "_" so e.g. e2e/npm_translate_lock_git+ssh becomes
+                  # a valid e2e-npm_translate_lock_git_ssh slug.
                   entries=()
                   for p in "${paths[@]}"; do
                     if [[ "$p" == "." ]]; then
@@ -126,6 +127,7 @@
                     else
                       slug=${p//\//-}
                       slug=${slug//+/_}
+                      slug=${slug//./_}
                     fi
                     entries+=( "{\"path\":\"$p\",\"slug\":\"$slug\"}" )
                   done
diff --git a/e2e/pnpm_lockfiles/v101/snapshots/is-odd-v0_resolved.json b/e2e/pnpm_lockfiles/v101/snapshots/is-odd-v0_resolved.json
index 95aed62..d39e89c 100644
--- a/e2e/pnpm_lockfiles/v101/snapshots/is-odd-v0_resolved.json
+++ b/e2e/pnpm_lockfiles/v101/snapshots/is-odd-v0_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v101/snapshots/is-odd_resolved.json b/e2e/pnpm_lockfiles/v101/snapshots/is-odd_resolved.json
index 77c99e4..4458f3c 100644
--- a/e2e/pnpm_lockfiles/v101/snapshots/is-odd_resolved.json
+++ b/e2e/pnpm_lockfiles/v101/snapshots/is-odd_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v101/snapshots/lodash_resolved.json b/e2e/pnpm_lockfiles/v101/snapshots/lodash_resolved.json
index 94c4d49..09720d0 100644
--- a/e2e/pnpm_lockfiles/v101/snapshots/lodash_resolved.json
+++ b/e2e/pnpm_lockfiles/v101/snapshots/lodash_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v110/snapshots/is-odd-v0_resolved.json b/e2e/pnpm_lockfiles/v110/snapshots/is-odd-v0_resolved.json
index 95aed62..d39e89c 100644
--- a/e2e/pnpm_lockfiles/v110/snapshots/is-odd-v0_resolved.json
+++ b/e2e/pnpm_lockfiles/v110/snapshots/is-odd-v0_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v110/snapshots/is-odd_resolved.json b/e2e/pnpm_lockfiles/v110/snapshots/is-odd_resolved.json
index 77c99e4..4458f3c 100644
--- a/e2e/pnpm_lockfiles/v110/snapshots/is-odd_resolved.json
+++ b/e2e/pnpm_lockfiles/v110/snapshots/is-odd_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v110/snapshots/lodash_resolved.json b/e2e/pnpm_lockfiles/v110/snapshots/lodash_resolved.json
index 94c4d49..09720d0 100644
--- a/e2e/pnpm_lockfiles/v110/snapshots/lodash_resolved.json
+++ b/e2e/pnpm_lockfiles/v110/snapshots/lodash_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v90/snapshots/is-odd-v0_resolved.json b/e2e/pnpm_lockfiles/v90/snapshots/is-odd-v0_resolved.json
index 95aed62..d39e89c 100644
--- a/e2e/pnpm_lockfiles/v90/snapshots/is-odd-v0_resolved.json
+++ b/e2e/pnpm_lockfiles/v90/snapshots/is-odd-v0_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-3RKAybrJxq3zCUC+TJ5Ao0sBsbacAT3OBeNVcCbsQsHUC70qWK2R4JsIvax4OTjeGWnB8FumAWATUtS1jd+KYw==","version":"0.1.0"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v90/snapshots/is-odd_resolved.json b/e2e/pnpm_lockfiles/v90/snapshots/is-odd_resolved.json
index 9a142ad..3526ac5 100644
--- a/e2e/pnpm_lockfiles/v90/snapshots/is-odd_resolved.json
+++ b/e2e/pnpm_lockfiles/v90/snapshots/is-odd_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJ<LOCKVERSION>yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-CQpnWPrDwmP1+SMHXZhtLtJ<LOCKVERSION>yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==","version":"3.0.1"}
\ No newline at end of file
diff --git a/e2e/pnpm_lockfiles/v90/snapshots/lodash_resolved.json b/e2e/pnpm_lockfiles/v90/snapshots/lodash_resolved.json
index 94c4d49..09720d0 100644
--- a/e2e/pnpm_lockfiles/v90/snapshots/lodash_resolved.json
+++ b/e2e/pnpm_lockfiles/v90/snapshots/lodash_resolved.json
@@ -1 +1 @@
-{"$schema":"https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
+{"$schema":"https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock","integrity":"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==","version":"file:../vendored/lodash-4.17.21.tgz"}
\ No newline at end of file
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/.bazelignore b/e2e/ts_version_from_rules_ts_3.8.10/.bazelignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/.bazelignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/.bazelrc b/e2e/ts_version_from_rules_ts_3.8.10/.bazelrc
new file mode 100644
index 0000000..58d7130
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/.bazelrc
@@ -0,0 +1,2 @@
+import %workspace%/../../tools/preset.bazelrc
+import %workspace%/../e2e.bazelrc
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/.bazelversion b/e2e/ts_version_from_rules_ts_3.8.10/.bazelversion
new file mode 100644
index 0000000..5942a0d
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/.bazelversion
@@ -0,0 +1 @@
+7.7.1
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/.npmrc b/e2e/ts_version_from_rules_ts_3.8.10/.npmrc
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/.npmrc
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/BUILD.bazel b/e2e/ts_version_from_rules_ts_3.8.10/BUILD.bazel
new file mode 100644
index 0000000..40da6ab
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
+load("@bazel_skylib//rules:build_test.bzl", "build_test")
+load("@npm//:defs.bzl", "npm_link_all_packages")
+
+npm_link_all_packages(name = "node_modules")
+
+ts_project(
+    name = "lib",
+    srcs = ["main.ts"],
+    transpiler = "tsc",
+    tsconfig = "tsconfig.json",
+)
+
+build_test(
+    name = "test",
+    targets = [":lib"],
+)
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/MODULE.bazel b/e2e/ts_version_from_rules_ts_3.8.10/MODULE.bazel
new file mode 100644
index 0000000..db176cd
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/MODULE.bazel
@@ -0,0 +1,47 @@
+"""Regression test for https://github.com/aspect-build/rules_js/issues/2877
+
+rules_ts reads the `$schema` value of the `resolved.json` files generated by
+npm_translate_lock as the format discriminator for its documented
+`ts_version_from = "@npm//:typescript/resolved.json"` pattern, so the value
+must remain stable across rules_js releases.
+"""
+
+bazel_dep(name = "aspect_rules_js", version = "0.0.0", dev_dependency = True)
+local_path_override(
+    module_name = "aspect_rules_js",
+    path = "../..",
+)
+
+bazel_dep(name = "aspect_rules_ts", version = "3.8.10", dev_dependency = True)
+bazel_dep(name = "bazel_skylib", version = "1.5.0", dev_dependency = True)
+
+pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
+pnpm.pnpm(
+    name = "pnpm",
+    pnpm_version = "9.15.9",
+    pnpm_version_integrity = "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==",
+)
+use_repo(pnpm, "pnpm")
+
+npm = use_extension(
+    "@aspect_rules_js//npm:extensions.bzl",
+    "npm",
+    dev_dependency = True,
+)
+npm.npm_translate_lock(
+    name = "npm",
+    npmrc = "//:.npmrc",
+    pnpm_lock = "//:pnpm-lock.yaml",
+    verify_node_modules_ignored = "//:.bazelignore",
+)
+use_repo(npm, "npm")
+
+rules_ts_ext = use_extension(
+    "@aspect_rules_ts//ts:extensions.bzl",
+    "ext",
+    dev_dependency = True,
+)
+rules_ts_ext.deps(
+    ts_version_from = "@npm//:typescript/resolved.json",
+)
+use_repo(rules_ts_ext, "npm_typescript")
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/main.ts b/e2e/ts_version_from_rules_ts_3.8.10/main.ts
new file mode 100644
index 0000000..51a43b4
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/main.ts
@@ -0,0 +1 @@
+export const greeting: string = 'hello'
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/package.json b/e2e/ts_version_from_rules_ts_3.8.10/package.json
new file mode 100644
index 0000000..d817173
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/package.json
@@ -0,0 +1,9 @@
+{
+    "private": true,
+    "pnpm": {
+        "onlyBuiltDependencies": []
+    },
+    "devDependencies": {
+        "typescript": "5.8.3"
+    }
+}
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/pnpm-lock.yaml b/e2e/ts_version_from_rules_ts_3.8.10/pnpm-lock.yaml
new file mode 100644
index 0000000..ef1fc18
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/pnpm-lock.yaml
@@ -0,0 +1,24 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    devDependencies:
+      typescript:
+        specifier: 5.8.3
+        version: 5.8.3
+
+packages:
+
+  typescript@5.8.3:
+    resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+snapshots:
+
+  typescript@5.8.3: {}
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/pnpm-workspace.yaml b/e2e/ts_version_from_rules_ts_3.8.10/pnpm-workspace.yaml
new file mode 100644
index 0000000..2cce0eb
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+packages:
+    - '.'
diff --git a/e2e/ts_version_from_rules_ts_3.8.10/tsconfig.json b/e2e/ts_version_from_rules_ts_3.8.10/tsconfig.json
new file mode 100644
index 0000000..56d9d8e
--- /dev/null
+++ b/e2e/ts_version_from_rules_ts_3.8.10/tsconfig.json
@@ -0,0 +1,5 @@
+{
+    "compilerOptions": {
+        "types": []
+    }
+}
diff --git a/npm/private/npm_translate_lock_generate.bzl b/npm/private/npm_translate_lock_generate.bzl
index fc88492..78e28dd 100644
--- a/npm/private/npm_translate_lock_generate.bzl
+++ b/npm/private/npm_translate_lock_generate.bzl
@@ -314,8 +314,11 @@
                 resolved_json_file_path = "{}/{}".format(link_package, resolved_json_rel_path) if link_package else resolved_json_rel_path
 
                 final_rctx_files[resolved_json_file_path] = json.encode({
-                    # Allow consumers to auto-detect this filetype
-                    "$schema": "https://docs.aspect.build/bazel/javascript/aspect_rules_js/npm_extensions#tag-npm_translate_lock",
+                    # Allow consumers to auto-detect this filetype.
+                    # This value is a format discriminator relied upon by consumers (e.g. rules_ts
+                    # `ts_version_from`, see https://github.com/aspect-build/rules_js/issues/2877)
+                    # and must NOT change, even if the docs URL it once pointed to moves.
+                    "$schema": "https://docs.aspect.build/rules/aspect_rules_js/docs/npm_translate_lock",
                     "version": _import.version,
                     "integrity": _import.integrity,
                 })