From b47ede011aa38658345c477baa72953f6e779b5b Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Tue, 15 Sep 2026 06:29:06 +0800 Subject: [PATCH] build(site): [SITE-07] reproduce console-feed vendor modules --- .../baselines/console-feed-provenance.json | 366 ++++++++++++++++++ .../console-feed-3.2.2-LICENSE.txt | 21 + .../2026-09-15-SITE-07-console-feed-source.md | 34 ++ refactor/console-modernization.md | 5 +- refactor/plan.md | 2 +- refactor/progress.md | 7 + refactor/risks.json | 8 +- refactor/tasks.json | 4 +- refactor/third-party.json | 10 +- scripts/site-vendor/console/README.md | 31 ++ scripts/site-vendor/console/provenance.ts | 84 ++++ scripts/site-vendor/console/reproduce.ts | 60 +++ test/site-console.test.js | 40 ++ 13 files changed, 663 insertions(+), 9 deletions(-) create mode 100644 refactor/baselines/console-feed-provenance.json create mode 100644 refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt create mode 100644 refactor/changes/2026-09-15-SITE-07-console-feed-source.md create mode 100644 scripts/site-vendor/console/provenance.ts create mode 100644 scripts/site-vendor/console/reproduce.ts diff --git a/refactor/baselines/console-feed-provenance.json b/refactor/baselines/console-feed-provenance.json new file mode 100644 index 000000000..508368744 --- /dev/null +++ b/refactor/baselines/console-feed-provenance.json @@ -0,0 +1,366 @@ +{ + "schemaVersion": 1, + "task": "SITE-07", + "recordedAt": "2026-09-14T22:23:26.058Z", + "frozen": { + "path": "refactor/baselines/site-vendor/console-original.js", + "sha256": "e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03" + }, + "archive": { + "name": "console-feed", + "version": "3.2.2", + "tarball": "https://registry.npmjs.org/console-feed/-/console-feed-3.2.2.tgz", + "integrity": "sha512-/DxoNG48E+eIMhD9/lBftpJoFQI1e5TOXCwmwG9VIMXAVJPDyGRoY844eClkZBC4b4Ae24ndua6ui/E0jfrH9Q==", + "sha256": "3e02b8e4603fab1bc67d6cf99331a2b9470ea448fbcb3c6ef66615b03385f772" + }, + "compiler": { + "archive": { + "name": "terser", + "version": "3.17.0", + "tarball": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", + "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", + "sha256": "42e8fc6a9d1d11762cf1e8ce4c378bce2eaeea28578dd112a58de34a0a9cef8d" + }, + "member": "package/dist/bundle.min.js", + "sha256": "6dfee6a315a7928b6fc06fb306037c528fa11a3e8413b5608722a0eca2fa08db", + "sourceMapVersion": "0.6.1", + "options": { + "warnings": true, + "safari10": true, + "mangle": { + "toplevel": true + } + }, + "recipeSource": { + "archive": { + "name": "parcel-bundler", + "version": "1.12.5", + "tarball": "https://registry.npmjs.org/parcel-bundler/-/parcel-bundler-1.12.5.tgz", + "integrity": "sha512-hpku8mW67U6PXQIenW6NBbphBOMb8XzW6B9r093DUhYj5GN2FUB/CXCiz5hKoPYUsusZ35BpProH8AUF9bh5IQ==", + "sha256": "2999bb924723fe5d33dea65361c5d235e79912f778b712f30382eb4e9d156406" + }, + "member": "package/src/transforms/terser.js", + "sha256": "508d575d7428119cafc1a1edcc6a9f8e05653e9901cf04ee3b909145ecdb6147" + }, + "limitation": "This is an exact reproducing transform, not recovery of the original full Parcel lockfile or proof of a uniquely installed version." + }, + "sources": [ + { + "id": "m6b6", + "member": "package/lib/index.js", + "sourceSha256": "b0b79bae841edf4084e7f884da4aab9349f0d9e7af656fadda6ad26b8e4dd916", + "generatedSha256": "5be990ed02829d295cc31a6e3fde121dffc53a4b663d70af4954c9ae75d40c79", + "bodySha256": "5be990ed02829d295cc31a6e3fde121dffc53a4b663d70af4954c9ae75d40c79" + }, + { + "id": "yCSh", + "member": "package/lib/Component/index.js", + "sourceSha256": "c8b99fb95f5272ad6f4e152945e19dca6023f28b34e9c2bf0dda68738c978cf1", + "generatedSha256": "803744445c69187b7d762e67b72f20585dcbf9908ee5fd026907aafb4acbab58", + "bodySha256": "803744445c69187b7d762e67b72f20585dcbf9908ee5fd026907aafb4acbab58" + }, + { + "id": "VJzx", + "member": "package/lib/Component/theme/default.js", + "sourceSha256": "525ea614eed82fa24bc61ec8fb208067b463395bb9f69dca0ace0fe97530475e", + "generatedSha256": "af79c6085e219d832215950b61b7876848a58a7ae5c9220fe744c8a894bcbc78", + "bodySha256": "af79c6085e219d832215950b61b7876848a58a7ae5c9220fe744c8a894bcbc78" + }, + { + "id": "F6B0", + "member": "package/lib/Component/elements.js", + "sourceSha256": "b50350173c6c177caa7117011e8a9f811f040f8091bb0b0d4c6156d8a88bf3e7", + "generatedSha256": "04e268efa37ec3e22087dfec429bd9d2f36c8df17d538e2861b48c6ffa8ed176", + "bodySha256": "04e268efa37ec3e22087dfec429bd9d2f36c8df17d538e2861b48c6ffa8ed176" + }, + { + "id": "MnJh", + "member": "package/lib/Component/theme/index.js", + "sourceSha256": "bffac1fce38fae8b48c7085a3223ed83c669c1f8e9a76cc3ecc2612b241868a5", + "generatedSha256": "215cc714ebd19bcd842cde8acb83585958ba6989953055c1c5ca5fa65c8617c6", + "bodySha256": "215cc714ebd19bcd842cde8acb83585958ba6989953055c1c5ca5fa65c8617c6" + }, + { + "id": "VMze", + "member": "package/lib/Component/Message.js", + "sourceSha256": "21aa511c431b49b99fd19014a34b5fc2364f4789529986e7ac5dbfb2731330dd", + "generatedSha256": "d83cc7fadffee9335a9aadf788a21162d3cd4a02c7748396001b5d6eb1a6fd70", + "bodySha256": "d83cc7fadffee9335a9aadf788a21162d3cd4a02c7748396001b5d6eb1a6fd70" + }, + { + "id": "ftAA", + "member": "package/lib/Component/message-parsers/Formatted.js", + "sourceSha256": "c2f5771a1b1f074f9aa173efecb6599ba7b7a3ccfdb35113b97c2611334259a2", + "generatedSha256": "dc618144a1803b7f1c7981a2b2d309af45773864510f2c180167c84df33dfbdd", + "bodySha256": "dc618144a1803b7f1c7981a2b2d309af45773864510f2c180167c84df33dfbdd" + }, + { + "id": "R2hN", + "member": "package/lib/Component/react-inspector/elements.js", + "sourceSha256": "3db15ae3f770a3278a829fecaecb3e527c2294344a115f3123774a5865c5cebf", + "generatedSha256": "977afb19a0ac303a0bc51cf530bb8243ae08def67de7a270ffb33796fc8fa27d", + "bodySha256": "977afb19a0ac303a0bc51cf530bb8243ae08def67de7a270ffb33796fc8fa27d" + }, + { + "id": "CwU9", + "member": "package/lib/Component/devtools-parser/index.js", + "sourceSha256": "79932dab131a76a6bc4288b69674cc348680cbc700acd1b3ff8f3b675c6f4a8d", + "generatedSha256": "f634855517a582f02f585c9e7c5bf9edb0dee11be8bf2cc3109dd4ec82e6de83", + "bodySha256": "f634855517a582f02f585c9e7c5bf9edb0dee11be8bf2cc3109dd4ec82e6de83" + }, + { + "id": "SENo", + "member": "package/lib/Component/devtools-parser/format-message.js", + "sourceSha256": "9d989597b6c3d30ca84baac5f67e2014a6bac48ab5d1260dbce40cba7568155a", + "generatedSha256": "c5c66a439a3626150db2f3530874dddbffe88c420140802e0cdb2531ae2c976c", + "bodySha256": "c5c66a439a3626150db2f3530874dddbffe88c420140802e0cdb2531ae2c976c" + }, + { + "id": "uxdN", + "member": "package/lib/Component/devtools-parser/string-utils.js", + "sourceSha256": "1daafcdb9cdeab609e5c4561903d821acd46b6edac8a84bbaf93fc710ce3e99d", + "generatedSha256": "5aa9396d427f60f0ad543a58692b3b1e7888bfe75d32113cc28aabdbc21f8307", + "bodySha256": "5aa9396d427f60f0ad543a58692b3b1e7888bfe75d32113cc28aabdbc21f8307" + }, + { + "id": "LmXR", + "member": "package/lib/Component/message-parsers/Object.js", + "sourceSha256": "10009010bdc053a6c15e2e083457082f4fbff6ebab15f4a2efc02422f3a5b203", + "generatedSha256": "8d908461fa424ffa99856a00f06a26900d65d5333879e8ba0248075eb5cbd4a8", + "bodySha256": "8d908461fa424ffa99856a00f06a26900d65d5333879e8ba0248075eb5cbd4a8" + }, + { + "id": "j7Pd", + "member": "package/lib/Component/react-inspector/index.js", + "sourceSha256": "17de610ab22242607699955f71df7b6ae3f35e0017da51106e090864a80bf42f", + "generatedSha256": "0a6438f055de8fd999d686155b8b021099125c06f92728f7cd6c20c6bf5e403c", + "bodySha256": "0a6438f055de8fd999d686155b8b021099125c06f92728f7cd6c20c6bf5e403c" + }, + { + "id": "swKK", + "member": "package/lib/Component/message-parsers/Error.js", + "sourceSha256": "d13f4124372f1d5941e968c8d3403372056b3479668889acbd1768bb73bbe080", + "generatedSha256": "aa196768776c196a76dd1c4cc61bbd667159a12e13ec58a14d8e10ea09409be9", + "bodySha256": "aa196768776c196a76dd1c4cc61bbd667159a12e13ec58a14d8e10ea09409be9" + }, + { + "id": "pYO5", + "member": "package/lib/Hook/index.js", + "sourceSha256": "63243dff52752b4c8552924f486e93483bfdc0cb7739692d897fa799ac7abd0e", + "generatedSha256": "5475966f1b782a13aab14c65db35ffc6e1e2d1f4a74e535323babf4459eaabfb", + "bodySha256": "5475966f1b782a13aab14c65db35ffc6e1e2d1f4a74e535323babf4459eaabfb" + }, + { + "id": "V2JG", + "member": "package/lib/definitions/Methods.js", + "sourceSha256": "3e0373b4760fcb01db1ae5b6d03f9640b699dd87cda382113081b5f97f59a822", + "generatedSha256": "4476f5ab9cb1d0fa4d8b30cdda836398c84ebab3083bd6eabb1eb8a218756b6f", + "bodySha256": "4476f5ab9cb1d0fa4d8b30cdda836398c84ebab3083bd6eabb1eb8a218756b6f" + }, + { + "id": "RxPG", + "member": "package/lib/Hook/parse/index.js", + "sourceSha256": "bd8edf290bd950f1630be5a8a1c9c2f844bdf32f43df1fb2aba119d35e01ada4", + "generatedSha256": "8b055b5ad031122364b701efe0dbd37749a04dc1e968fc389b515ba9662058b7", + "bodySha256": "8b055b5ad031122364b701efe0dbd37749a04dc1e968fc389b515ba9662058b7" + }, + { + "id": "kEc0", + "member": "package/lib/Hook/parse/GUID.js", + "sourceSha256": "78dca26cc852b8d05c422197318a00cab1ff065c3a82ff12f6d586ca50e60dde", + "generatedSha256": "f61e1118796bd5329c893f7aa870f1d1f9439eff6cc2e0a9f329f1624494cb3f", + "bodySha256": "f61e1118796bd5329c893f7aa870f1d1f9439eff6cc2e0a9f329f1624494cb3f" + }, + { + "id": "b9Ac", + "member": "package/lib/Hook/parse/methods/timing.js", + "sourceSha256": "3ec9b76ba0ee3ef27753162fa720a33770cfe19cbc15316f26b406a3417d33ef", + "generatedSha256": "4ef00880c4faebabcc2296d4b0d5af3687599c44cf9ebc18b9f5d640a81bc557", + "bodySha256": "4ef00880c4faebabcc2296d4b0d5af3687599c44cf9ebc18b9f5d640a81bc557" + }, + { + "id": "tUcB", + "member": "package/lib/Hook/store/state.js", + "sourceSha256": "5e94f4ebb046796ccaf2424941c716e12fe454c3eb7982ee896b6a7a115a5666", + "generatedSha256": "063cbbd0e15bf5f66ea9839b1aef22c645e83c847106950bfba22a49c5e6aaeb", + "bodySha256": "063cbbd0e15bf5f66ea9839b1aef22c645e83c847106950bfba22a49c5e6aaeb" + }, + { + "id": "KD8b", + "member": "package/lib/Hook/store/dispatch.js", + "sourceSha256": "9422017cb57271d022b942f9cb5595e784e5e882940dda4234f458ed85c3c9f3", + "generatedSha256": "0cad70e5ae872d671ce41ec0ba3f07047b2eb31a2cb7c6dec7af14ef6874c125", + "bodySha256": "0cad70e5ae872d671ce41ec0ba3f07047b2eb31a2cb7c6dec7af14ef6874c125" + }, + { + "id": "AXxh", + "member": "package/lib/Hook/store/reducer.js", + "sourceSha256": "d2c1e1dd8424c8adf7c41ac87dbf0d1515851927a300706bb463c1136dd113b8", + "generatedSha256": "5a20f5f349cfe5341d8d7b91556012fa5eb7862b7727f323aed50746bfd2dd23", + "bodySha256": "5a20f5f349cfe5341d8d7b91556012fa5eb7862b7727f323aed50746bfd2dd23" + }, + { + "id": "nj54", + "member": "package/lib/Hook/store/actions.js", + "sourceSha256": "d94d4e1ac65d9ec2f44bf7bbddcd381f00fe6462e5b8ffc3efbad9339be24a3f", + "generatedSha256": "abce408bb92e92c9adf097ac3984e5d052c9c8f5d3e69c03404341b82a94466e", + "bodySha256": "abce408bb92e92c9adf097ac3984e5d052c9c8f5d3e69c03404341b82a94466e" + }, + { + "id": "BQYC", + "member": "package/lib/Hook/parse/methods/count.js", + "sourceSha256": "61fb64e0ce485f19b058d505d84a60ee63fa4069c235577f7da4ae3824f18881", + "generatedSha256": "bd7e9e0250873cc7aa48af4733c7f2efc1f48dda58c8e6198a335395f0616183", + "bodySha256": "bd7e9e0250873cc7aa48af4733c7f2efc1f48dda58c8e6198a335395f0616183" + }, + { + "id": "dKro", + "member": "package/lib/Hook/parse/methods/assert.js", + "sourceSha256": "1d80efcffc998c08ce30aceb3fb880222fbc604304363829a88bd06a4efb5847", + "generatedSha256": "25730ae6159aeb1da85ce13a63ef7f36fe12f9e86c8a5f39eab5278f2531ea9c", + "bodySha256": "25730ae6159aeb1da85ce13a63ef7f36fe12f9e86c8a5f39eab5278f2531ea9c" + }, + { + "id": "Zbbf", + "member": "package/lib/Transform/index.js", + "sourceSha256": "87c7c0f901c7c3fe461cbddf344dda85e0bbac09808bf1224ff3dfa034b371c9", + "generatedSha256": "a6e8f7ee72f2bfa0747c4cc0fb8aea6df3f2c2b362248d6d64c39e70b3109f46", + "bodySha256": "a6e8f7ee72f2bfa0747c4cc0fb8aea6df3f2c2b362248d6d64c39e70b3109f46" + }, + { + "id": "dq61", + "member": "package/lib/Transform/arithmetic.js", + "sourceSha256": "e8a6d77a65fc8ae73cdc5b221c4ee75d72ce32a2b8acf544796b72c55936827b", + "generatedSha256": "02b98dd6f320c114767fa8fcf8a470907df7f8091a41d84430007209c1d6b181", + "bodySha256": "02b98dd6f320c114767fa8fcf8a470907df7f8091a41d84430007209c1d6b181" + }, + { + "id": "XwmA", + "member": "package/lib/Transform/Function.js", + "sourceSha256": "10a220fd9dd6f14e0af03f552316ed1cf12d61846956fb989d419661dbea0de0", + "generatedSha256": "199b2c52101cf2f9bbfc33ff516eca6e7e7fff077ba41b52a427ca8ef960b5b7", + "bodySha256": "199b2c52101cf2f9bbfc33ff516eca6e7e7fff077ba41b52a427ca8ef960b5b7" + }, + { + "id": "GDCg", + "member": "package/lib/Transform/HTML.js", + "sourceSha256": "de476cec047de1da472ccfc766535ed4bc627ebf168a85f8ad4d5e35b13fa834", + "generatedSha256": "70dd97167ccc13b9be60eea8a24a2bdcea122c6688c9fb58e4e63e721f40b72f", + "bodySha256": "70dd97167ccc13b9be60eea8a24a2bdcea122c6688c9fb58e4e63e721f40b72f" + }, + { + "id": "wqxJ", + "member": "package/lib/Transform/Map.js", + "sourceSha256": "cc9c19079569321216ce6ccd3bee4d4821e68882d3123d01b67bf8df7701b4d4", + "generatedSha256": "f5bb3bb16f6971cfa779dc7acc02f74b4bf9cb2e81c5872df3336d4f4ab41e82", + "bodySha256": "f5bb3bb16f6971cfa779dc7acc02f74b4bf9cb2e81c5872df3336d4f4ab41e82" + }, + { + "id": "HTuI", + "member": "package/lib/Transform/replicator/index.js", + "sourceSha256": "33ea546afdd46833594d7cdccbfc45dc654b8e9fa737d9ee5d940b6f164dba6b", + "generatedSha256": "97f6bd7b15087b2a042d3d4fb8abfaa8c7add5f18ac7e5eff767988708bbdc3a", + "bodySha256": "97f6bd7b15087b2a042d3d4fb8abfaa8c7add5f18ac7e5eff767988708bbdc3a" + }, + { + "id": "aGBs", + "member": "package/lib/Unhook/index.js", + "sourceSha256": "47e84dcb6c2a0a752eb778e8fab484d1efd363c0284fde0b1fa28a4b6d98503c", + "generatedSha256": "91261579fb7d4bada1cd6e09f660397d4cff10838e100157ea375e48ec38d365", + "bodySha256": "91261579fb7d4bada1cd6e09f660397d4cff10838e100157ea375e48ec38d365" + } + ], + "external": [ + { + "from": "yCSh", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "yCSh", + "dependency": "emotion-theming", + "id": "pufF" + }, + { + "from": "VJzx", + "dependency": "react-inspector", + "id": "kuYm" + }, + { + "from": "MnJh", + "dependency": "@emotion/styled", + "id": "IX9o" + }, + { + "from": "VMze", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "VMze", + "dependency": "emotion-theming", + "id": "pufF" + }, + { + "from": "ftAA", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "CwU9", + "dependency": "linkifyjs/html", + "id": "SbWR" + }, + { + "from": "LmXR", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "LmXR", + "dependency": "emotion-theming", + "id": "pufF" + }, + { + "from": "LmXR", + "dependency": "linkifyjs/react", + "id": "h3Cb" + }, + { + "from": "j7Pd", + "dependency": "emotion-theming", + "id": "pufF" + }, + { + "from": "j7Pd", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "j7Pd", + "dependency": "react-inspector", + "id": "kuYm" + }, + { + "from": "swKK", + "dependency": "react", + "id": "n8MK" + }, + { + "from": "swKK", + "dependency": "linkifyjs/react", + "id": "h3Cb" + } + ], + "notice": { + "source": "refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt", + "member": "package/LICENSE", + "sha256": "52412d7bc7ce4157ea628bbaacb8829e0a9cb3c58f57f99176126bc8cf2bfc85", + "status": "Preserved verbatim upstream MIT text and Facebook attribution. Full bundled-component provenance/attribution remains open, including embedded replicator and all external dependencies." + }, + "limits": [ + "32 console-feed module bodies match; 68 other vendor modules and the Parcel wrapper still require complete attribution/provenance.", + "No complete license closure or runtime update.", + "No execution of archived console-feed runtime during reconstruction." + ] +} diff --git a/refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt b/refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt new file mode 100644 index 000000000..b96dcb048 --- /dev/null +++ b/refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/refactor/changes/2026-09-15-SITE-07-console-feed-source.md b/refactor/changes/2026-09-15-SITE-07-console-feed-source.md new file mode 100644 index 000000000..2c94e6721 --- /dev/null +++ b/refactor/changes/2026-09-15-SITE-07-console-feed-source.md @@ -0,0 +1,34 @@ +# SITE-07 console-feed 源码重建检查点 + +官方 npm console-feed 3.2.2 归档通过 SHA-512 SRI 校验。由 m6b6 沿全部相对依赖 +遍历得到 32 个模块;使用官方 Terser 3.17.0 和 Parcel 1.12.5 中的压缩参数, +32 个输出函数体与冻结 console bundle 全部逐字一致。原始归档、成员和编译结果 +指纹、外部依赖边界见[来源记录](../baselines/console-feed-provenance.json)。 + +这是直接源码匹配证据;没有找回原始安装锁,也不能证明同样代码不存在于其他 +历史版本。记录中的 Parcel 是能复现的参数来源,不冒充已确认的原始构建版本。 +完整 console 有 100 个第三方模块,剩余 68 个及 Parcel wrapper 仍需来源核查。 + +## 实现与验证 + +provenance.ts 提取固定模块表并验证完整遍历、源文件映射、外部依赖、源码与输出 +哈希和实际字节。reproduce.ts 通过显式 --fetch 准备两个固定归档,随后可完全 +离线重建;只加载已校验的历史编译器,不执行 console-feed 的运行时代码,不安装 +依赖。既有 source-map 0.6.1 来自根 Yarn 锁及已安装树,版本不符会拒绝。 + +首次下载重建和离线重建均为 32/32 精确匹配。新增反例覆盖归档变更、弱 SRI、 +缺模块/映射、重复/不可达条目、外部边界漂移、源码/编译输出/旧函数体漂移。 +控制台单元共 9/9 通过,严格 docs-tools TS、相关 lint 和 build:console --check +通过。初期使用当前 Terser 5.51.2 产生形式差异;未因此放宽比较,而是读取历史 +Parcel 的压缩器依赖及参数,最终获得字节一致。初次归档路径误选目录和两处 lint +格式错误已修正,不影响最终结果。 + +上游 LICENSE 原文已保存,保留其中 Facebook 署名。其嵌入 replicator 和其他 +组件的来源/署名仍未全部审查,不把单个 MIT 文件当作整个 console 的完整许可。 +VENDOR-08 保持 open;SITE-07 保持 doing,完成任务数仍为 199/265。 + +本批没有改动站点运行时、公开 API、依赖清单或锁文件,因此未重复实际页面测试。 +上一任务的浏览器证据继续对应相同产物;本批不声称新增浏览器/远端 CI/发布验收。 +复现命令和后续维护见[说明](../../scripts/site-vendor/console/README.md)。 +回退本检查点只移除复现工具、来源/许可快照、测试和台账更新;不会恢复旧缺陷。 +作为 SITE-07 的独立检查点本地提交,无推送或发布。 diff --git a/refactor/console-modernization.md b/refactor/console-modernization.md index 73463cb82..cd80dc7bb 100644 --- a/refactor/console-modernization.md +++ b/refactor/console-modernization.md @@ -10,8 +10,9 @@ SITE-07 冻结原始 bundle 和浏览器契约,SITE-CONSOLE-01 已实现自有 - Focm 是自有安装入口,W5CS 是自有 React 控制台视图,其余模块须继续溯源。 - 实际全局包括 React、ReactDOM、consoleLog 和 parcelRequire。React/ReactDOM 均为 17.0.2,styled-components bundle 声明版本 5.3.3。 -- console-feed 的包版本不能只凭年代推定;npm 当时最新版本 3.2.2 是待比对候选, - 本批没有确认它就是当前捆绑版本,也没有确认全部传递依赖的版本与许可。 +- console-feed 的 32 个模块现已用官方 3.2.2 归档精确重建,见 + [来源记录](baselines/console-feed-provenance.json)。这证明源码匹配,不代表找回 + 原始 lockfile 或证明唯一安装版本;其余传递依赖和完整许可仍待核查。 - 原末尾指向不存在的 /index.js.map。原始 bundle 和模块指纹见 [冻结来源](baselines/site-console-inventory.json)。 diff --git a/refactor/plan.md b/refactor/plan.md index 795dd3b41..535e90067 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -653,7 +653,7 @@ - SITE-AI-DOCS-01: [记录](changes/2026-09-14-SITE-AI-DOCS-01-documentation-pipeline.md) [记录](baselines/documentation-pipeline-validation.json) - SITE-BUILD-01: [记录](changes/2026-09-14-SITE-BUILD-01-staged-builds.md) [记录](baselines/site-build-validation.json) - SITE-03: [记录](changes/2026-09-14-SITE-03-desktop-editor.md) [记录](baselines/site-editor-validation.json) -- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json) [记录](changes/2026-09-14-SITE-07-vendor-notices.md) [记录](baselines/site-notices-checkpoint.json) [记录](baselines/site-codicons-provenance.json) [记录](baselines/site-codicons-validation.json) [记录](changes/2026-09-15-SITE-07-codicons.md) [记录](baselines/vconsole-notices-provenance.json) [记录](baselines/vconsole-notices-validation.json) [记录](changes/2026-09-15-SITE-07-vconsole-notices.md) [记录](console-modernization.md) [记录](baselines/site-console-inventory.json) [记录](baselines/site-console-validation.json) [记录](changes/2026-09-15-SITE-07-console-baseline.md) +- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json) [记录](changes/2026-09-14-SITE-07-vendor-notices.md) [记录](baselines/site-notices-checkpoint.json) [记录](baselines/site-codicons-provenance.json) [记录](baselines/site-codicons-validation.json) [记录](changes/2026-09-15-SITE-07-codicons.md) [记录](baselines/vconsole-notices-provenance.json) [记录](baselines/vconsole-notices-validation.json) [记录](changes/2026-09-15-SITE-07-vconsole-notices.md) [记录](console-modernization.md) [记录](baselines/site-console-inventory.json) [记录](baselines/site-console-validation.json) [记录](changes/2026-09-15-SITE-07-console-baseline.md) [记录](baselines/console-feed-provenance.json) [记录](changes/2026-09-15-SITE-07-console-feed-source.md) - EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs) - EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs) - MOD-01: [记录](changes/2026-09-15-MOD-01-bun-evaluation.md) [记录](baselines/bun-install-validation.json) [记录](bun-evaluation.md) diff --git a/refactor/progress.md b/refactor/progress.md index cbccc4ba9..25f234e80 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,12 @@ # 进度与证据 +## SITE-07 console-feed 32 模块精确重建 + +官方 npm 3.2.2 的 32 个模块使用历史压缩配置全部重建为旧 bundle 原字节;首次 +下载和离线复现均通过,新增完整依赖遍历及篡改反例,单元 9/9。来源与许可原文 +已保存,见[记录](changes/2026-09-15-SITE-07-console-feed-source.md)。其余 68 个 +第三方模块/Parcel 和完整许可仍开放;SITE-07 doing,仍为199/265,无运行时变更。 + ## SITE-CONSOLE-01 自有 TS 控制台与三项修复 完成入口/视图/订阅/错误/样式/类型拆分,保留原 React17 与 100 个第三方模块。 diff --git a/refactor/risks.json b/refactor/risks.json index 25ea929cc..f12fe43eb 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -681,11 +681,15 @@ "refactor/baselines/site-console-inventory.json", "refactor/baselines/site-console-validation.json", "refactor/changes/2026-09-15-SITE-07-console-baseline.md", - "test/browser/site-console.spec.js" + "test/browser/site-console.spec.js", + "refactor/baselines/console-feed-provenance.json", + "refactor/changes/2026-09-15-SITE-07-console-feed-source.md", + "scripts/site-vendor/console/provenance.ts", + "scripts/site-vendor/console/reproduce.ts" ], "compatibleResolution": "Keep separate from owned TS migration. Verify source/version/diff and license notices before replacement; preserve API/CSS/worker URLs and run owning package tests.", "closureCriteria": "固定上游版本/内容差异、完整组件许可与分发 notices,兼容测试通过;仅当前上游许可证名称不足以关闭。", - "workspaceState": "Frozen 102-module Parcel bundle and real React 17.0.2/global/component/DOM behavior are recorded. Three lifecycle/error defects are separately reproduced. Remaining dependency versions, complete notices and reproducible source remain open; no runtime replacement yet." + "workspaceState": "32 console-feed modules match official 3.2.2 archive exactly under a verified historical transform. Other 68 vendor modules, Parcel wrapper, original lockfile and complete attribution remain open. Runtime files unchanged in this checkpoint." }, { "id": "SDK-01", diff --git a/refactor/tasks.json b/refactor/tasks.json index 5bccd652a..8394f6efb 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -4488,7 +4488,9 @@ "console-modernization.md", "baselines/site-console-inventory.json", "baselines/site-console-validation.json", - "changes/2026-09-15-SITE-07-console-baseline.md" + "changes/2026-09-15-SITE-07-console-baseline.md", + "baselines/console-feed-provenance.json", + "changes/2026-09-15-SITE-07-console-feed-source.md" ] }, { diff --git a/refactor/third-party.json b/refactor/third-party.json index 068dcfa97..e77b8c079 100644 --- a/refactor/third-party.json +++ b/refactor/third-party.json @@ -1060,9 +1060,13 @@ "owners": [ "SITE-01" ], - "sourceStatus": "Frozen original 340306-byte Parcel bundle e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03; 102-module map, Focm entry/W5CS view, React 17.0.2 globals and styled-components 5.3.3 literal identified. Full dependency versions and source reconstruction remain open. SITE-CONSOLE-01 replaces only Focm/W5CS bodies from owned TS and removes the missing source-map trailer; other 100 modules and runtime remain byte-for-byte frozen. Full vendor provenance remains open.", - "licenseStatus": "Root MIT is not sufficient evidence for all bundled components; inventory and notices pending.", - "upstreamSources": [], + "sourceStatus": "Frozen original 340306-byte Parcel bundle e00bbf82bf08c452825690372ded39a9b60f4baf438f0e7ce8300e9bc4629c03; 102-module map, Focm entry/W5CS view, React 17.0.2 globals and styled-components 5.3.3 literal identified. Full dependency versions and source reconstruction remain open. SITE-CONSOLE-01 replaces only Focm/W5CS bodies from owned TS and removes the missing source-map trailer; other 100 modules and runtime remain byte-for-byte frozen. Full vendor provenance remains open. Official console-feed 3.2.2 archive reproduces all 32 relative-import module bodies exactly with verified Terser 3.17.0/Parcel 1.12.5 transform settings; 68 vendor modules and wrapper remain. This does not recover the original lockfile or establish a unique installed version.", + "licenseStatus": "Official console-feed 3.2.2 MIT LICENSE preserved verbatim, including Facebook attribution. Embedded replicator and all external/remaining component notices still require review; no full closure.", + "upstreamSources": [ + "https://registry.npmjs.org/console-feed/-/console-feed-3.2.2.tgz", + "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", + "https://registry.npmjs.org/parcel-bundler/-/parcel-bundler-1.12.5.tgz" + ], "upstreamReviewedAt": null, "updatePolicy": "Keep separate from owned TS migration. Verify source/version/diff and license notices before replacement; preserve API/CSS/worker URLs and run owning package tests.", "fingerprints": [ diff --git a/scripts/site-vendor/console/README.md b/scripts/site-vendor/console/README.md index 6bb3d2f19..4df8a5a18 100644 --- a/scripts/site-vendor/console/README.md +++ b/scripts/site-vendor/console/README.md @@ -67,3 +67,34 @@ No new dependency is required. Existing esbuild 0.27.7 and TypeScript 5.9.3 prov generation and structural parsing. Full console dependency provenance and notices remain SITE-07 / VENDOR-08; this build does not close them. See `refactor/console-modernization.md` for contracts and evidence. + +## Historical source reconstruction + +`refactor/baselines/console-feed-provenance.json` maps all 32 modules reachable +through relative imports from m6b6 to the official console-feed 3.2.2 archive. +Their rebuilt bodies exactly match the frozen bundle. The 68 remaining vendor +modules and Parcel wrapper need further provenance; this does not establish a +unique original installed version or recover its missing lockfile. + +```sh +node scripts/site-vendor/console/reproduce.ts --fetch +node scripts/site-vendor/console/reproduce.ts +yarn test:site-console +``` + +The first command downloads two pinned npm archives into the dedicated ignored +`refactor/.cache/console-feed-reproduction/` directory. The second uses that cache +offline. Both verify SHA-512 SRI, archive SHA-256, source member fingerprints, +compiler bytes and exact output. They load Terser 3.17.0 as a historical build +tool with the already installed source-map 0.6.1; they do not install dependencies, +change Yarn or execute the archived console-feed runtime. Canonical Node is required. +The minification recipe is recovered from Parcel 1.12.5's archive; this proves a +reproducing transform, not that the original author used precisely that Parcel +version. `provenance.ts` separately verifies complete traversal, relative source +mapping and external dependency boundaries. Missing, unreachable, duplicate or +changed evidence fails instead of silently shrinking the comparison. + +The archived LICENSE is preserved verbatim with its Facebook attribution under +`refactor/baselines/site-vendor/console-feed-3.2.2-LICENSE.txt`. Do not rewrite it +or infer it covers every embedded/external component. Complete notices remain +open, including the bundled replicator and remaining dependencies. diff --git a/scripts/site-vendor/console/provenance.ts b/scripts/site-vendor/console/provenance.ts new file mode 100644 index 000000000..f2cd8d94a --- /dev/null +++ b/scripts/site-vendor/console/provenance.ts @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import path from 'node:path' +import ts from 'typescript' + +export interface Module { id: string, body: string, dependencies: Record } +export interface Source { id: string, member: string, sourceSha256: string, generatedSha256: string, bodySha256: string } +export interface External { from: string, dependency: string, id: string } +export interface Archive { name: string, version: string, tarball: string, integrity: string, sha256: string } +export const hash = (bytes: string | Uint8Array) => createHash('sha256').update(bytes).digest('hex') + +export function verifyArchive(bytes: Uint8Array, archive: Archive) { + const [algorithm, expected] = archive.integrity.split('-') + assert(algorithm === 'sha512' && expected, 'Expected SHA-512 archive integrity') + assert.equal(createHash(algorithm).update(bytes).digest('base64'), expected, `Archive integrity changed: ${archive.name}`) + assert.equal(hash(bytes), archive.sha256, `Archive bytes changed: ${archive.name}`) +} + +export function parcelModules(text: string): Map { + const source = ts.createSourceFile('console.js', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS) + let table: ts.ObjectLiteralExpression | undefined + function visit(node: ts.Node) { + if (ts.isObjectLiteralExpression(node) && node.properties.some(property => property.name && ts.isStringLiteral(property.name) && property.name.text === 'Focm')) { + assert(!table, 'Duplicate Parcel module tables') + table = node + } + ts.forEachChild(node, visit) + } + visit(source) + assert(table, 'Missing Parcel module table') + const result = new Map() + for (const property of table.properties) { + assert(ts.isPropertyAssignment(property) && ts.isStringLiteral(property.name) && ts.isArrayLiteralExpression(property.initializer)) + const [fn, deps] = property.initializer.elements + assert(fn && ts.isFunctionExpression(fn) && deps && ts.isObjectLiteralExpression(deps)) + const id = property.name.text + assert(!result.has(id), `Duplicate Parcel module: ${id}`) + const dependencies: Record = {} + for (const dep of deps.properties) { + assert(ts.isPropertyAssignment(dep) && ts.isStringLiteral(dep.name) && ts.isStringLiteral(dep.initializer)) + assert(!(dep.name.text in dependencies), 'Duplicate Parcel dependency') + dependencies[dep.name.text] = dep.initializer.text + } + result.set(id, { id, dependencies, body: text.slice(fn.body.getStart(source) + 1, fn.body.end - 1).trim() }) + } + return result +} + +export function verifyModules(modules: Map, sources: Source[], external: External[], read: (member: string) => string, compile: (source: string) => string) { + const mapped = new Map(sources.map(source => [source.id, source])) + assert.equal(mapped.size, sources.length, 'Duplicate source mapping') + const visited = new Set() + const dependencies: External[] = [] + function visit(id: string) { + if (visited.has(id)) + return + const item = mapped.get(id) + const module = modules.get(id) + assert(item && module, `Missing console-feed module mapping: ${id}`) + assert(item.member.startsWith('package/lib/') && item.member.endsWith('.js') && path.posix.normalize(item.member) === item.member, 'Invalid source member') + visited.add(id) + const source = read(item.member) + assert.equal(hash(source), item.sourceSha256, `Source changed: ${item.member}`) + const compiled = compile(source) + assert.equal(hash(compiled), item.generatedSha256, `Compiler output changed: ${id}`) + assert.equal(hash(module.body), item.bodySha256, `Frozen module changed: ${id}`) + assert.equal(compiled, module.body, `Rebuilt module differs: ${id}`) + for (const [dependency, child] of Object.entries(module.dependencies)) { + assert(modules.has(child), `Unresolved Parcel dependency: ${child}`) + if (!dependency.startsWith('.')) { + dependencies.push({ from: id, dependency, id: child }) + continue + } + const base = path.posix.join(path.posix.dirname(item.member), dependency) + const target = mapped.get(child)?.member + assert(target && [base, `${base}.js`, `${base}/index.js`].includes(target), `Source dependency mapping changed: ${id} ${dependency}`) + visit(child) + } + } + visit('m6b6') + assert.equal(visited.size, sources.length, 'Unreachable source mapping') + assert.deepEqual(dependencies, external, 'External dependency boundary changed') + return visited.size +} diff --git a/scripts/site-vendor/console/reproduce.ts b/scripts/site-vendor/console/reproduce.ts new file mode 100644 index 000000000..67451e711 --- /dev/null +++ b/scripts/site-vendor/console/reproduce.ts @@ -0,0 +1,60 @@ +import type { Archive, External, Source } from './provenance.ts' +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { hash, parcelModules, verifyArchive, verifyModules } from './provenance.ts' + +interface CompilerOptions { warnings: boolean, safari10: boolean, mangle: { toplevel: boolean } } +interface Provenance { + archive: Archive + frozen: { path: string, sha256: string } + compiler: { archive: Archive, member: string, sha256: string, sourceMapVersion: string, options: CompilerOptions } + sources: Source[] + external: External[] +} +interface Minifier { minify: (source: string, options: CompilerOptions) => { code?: string, error?: unknown } } + +const root = fileURLToPath(new URL('../../../', import.meta.url)) +const record: Provenance = JSON.parse(fs.readFileSync(path.join(root, 'refactor/baselines/console-feed-provenance.json'), 'utf8')) +assert(process.argv.slice(2).every(arg => arg === '--fetch'), 'Use reproduce.ts [--fetch]') +assert.equal(process.version, `v${fs.readFileSync(path.join(root, '.node-version'), 'utf8').trim()}`, 'Use canonical Node') +const cacheRoot = fs.realpathSync(path.join(root, 'refactor/.cache')) +const cache = path.join(cacheRoot, 'console-feed-reproduction') +fs.mkdirSync(cache, { recursive: true }) +assert.equal(fs.realpathSync(cache), cache, 'Reproduction cache must not redirect') +for (const archive of [record.archive, record.compiler.archive]) { + const target = path.join(cache, `${archive.name}-${archive.version}.tgz`) + if (process.argv.includes('--fetch')) { + const response = await fetch(archive.tarball) + assert(response.ok, `Archive request failed: ${response.status}`) + const bytes = new Uint8Array(await response.arrayBuffer()) + verifyArchive(bytes, archive) + fs.writeFileSync(target, bytes) + } + verifyArchive(fs.readFileSync(target), archive) +} +const readMember = (name: string, member: string) => execFileSync('tar', ['-xOzf', path.join(cache, name), member], { maxBuffer: 8 * 1024 * 1024 }) +const compilerBytes = readMember('terser-3.17.0.tgz', record.compiler.member) +assert.equal(hash(compilerBytes), record.compiler.sha256, 'Compiler member changed') +const compilerPath = path.join(cache, 'terser.cjs') +fs.writeFileSync(compilerPath, compilerBytes) +const require = createRequire(compilerPath) +assert.equal(require('source-map/package.json').version, record.compiler.sourceMapVersion, 'Source-map dependency changed') + +const { minify } = require(compilerPath) as Minifier + +const frozen = fs.readFileSync(path.join(root, record.frozen.path), 'utf8') +assert.equal(hash(frozen), record.frozen.sha256, 'Frozen console changed') +assert.equal(record.sources.length, 32, 'Incomplete console-feed scope') +const count = verifyModules(parcelModules(frozen), record.sources, record.external, member => readMember('console-feed-3.2.2.tgz', member).toString('utf8'), (source) => { + const result = minify(source, record.compiler.options) + if (result.error) + throw result.error + assert(typeof result.code === 'string') + return result.code +}) +console.log(JSON.stringify({ exactModules: count, totalVendorModules: 100, archive: record.archive.sha256, licenseClosure: false })) diff --git a/test/site-console.test.js b/test/site-console.test.js index 396a844bf..d9e00e57a 100644 --- a/test/site-console.test.js +++ b/test/site-console.test.js @@ -1,4 +1,5 @@ import assert from 'node:assert/strict' +import { Buffer } from 'node:buffer' import { createHash } from 'node:crypto' import fs from 'node:fs' import process from 'node:process' @@ -6,6 +7,7 @@ import process from 'node:process' import test from 'node:test' import ts from 'typescript' import { generateConsole, moduleRanges, obsoleteMap, upstreamSha256 } from '../scripts/site-vendor/console/build.ts' +import { hash, parcelModules, verifyArchive, verifyModules } from '../scripts/site-vendor/console/provenance.ts' import { errorArgument } from '../scripts/site-vendor/console/runtime/errors.ts' import { css } from '../scripts/site-vendor/console/runtime/style.ts' import { createSubscriptions } from '../scripts/site-vendor/console/runtime/subscriptions.ts' @@ -175,3 +177,41 @@ test('Console build changes only the two owned module bodies and is deterministi assert.notEqual(candidate, source) assert.throws(() => moduleRanges('var unrelated = 1'), /Missing owned/) }) + +test('Console provenance rejects altered archive bytes and non-SHA512 integrity', () => { + const bytes = Buffer.from('verified archive') + const archive = { name: 'fixture', sha256: hash(bytes), integrity: `sha512-${createHash('sha512').update(bytes).digest('base64')}` } + verifyArchive(bytes, archive) + assert.throws(() => verifyArchive(Buffer.from('changed archive'), archive), /integrity changed/) + assert.throws(() => verifyArchive(bytes, { ...archive, sha256: 'wrong' }), /bytes changed/) + assert.throws(() => verifyArchive(bytes, { ...archive, integrity: 'sha1-weak' }), /Expected SHA-512/) +}) + +test('Console provenance extracts the frozen 102 modules without executing vendor code', () => { + const source = fs.readFileSync('refactor/baselines/site-vendor/console-original.js', 'utf8') + const modules = parcelModules(source) + assert.equal(modules.size, 102) + assert.equal(modules.get('m6b6').dependencies['./Hook'], 'pYO5') + assert.throws(() => parcelModules('const unrelated = 1'), /Missing Parcel/) + assert.throws(() => parcelModules('({"Focm":[function(){},{}],"Focm":[function(){},{}]})'), /Duplicate Parcel module/) +}) + +test('Console provenance requires exact source, compiler output, dependency mapping and complete reachability', () => { + const root = { id: 'm6b6', body: 'exports.value=1;', dependencies: { './child': 'child', 'react': 'react' } } + const child = { id: 'child', body: 'exports.child=2;', dependencies: {} } + const modules = new Map([root, child, { id: 'react', body: '', dependencies: {} }].map(module => [module.id, module])) + const sources = [root, child].map(module => ({ id: module.id, member: `package/lib/${module.id === 'm6b6' ? 'index' : module.id}.js`, sourceSha256: hash(module.body), generatedSha256: hash(module.body), bodySha256: hash(module.body) })) + const external = [{ from: 'm6b6', dependency: 'react', id: 'react' }] + const read = member => member.endsWith('index.js') ? root.body : child.body + assert.equal(verifyModules(modules, sources, external, read, source => source), 2) + assert.throws(() => verifyModules(modules, sources, external, () => 'changed', source => source), /Source changed/) + assert.throws(() => verifyModules(modules, sources, external, read, () => 'changed'), /Compiler output changed/) + assert.throws(() => verifyModules(modules, [sources[0]], external, read, source => source), /Source dependency mapping changed/) + assert.throws(() => verifyModules(modules, [...sources, sources[0]], external, read, source => source), /Duplicate source/) + assert.throws(() => verifyModules(modules, [...sources, { ...sources[0], id: 'unused' }], external, read, source => source), /Unreachable/) + assert.throws(() => verifyModules(modules, sources, [], read, source => source), /External dependency boundary/) + const bad = [{ ...sources[0], member: 'package/lib/../escape.js' }, sources[1]] + assert.throws(() => verifyModules(modules, bad, external, read, source => source), /Invalid source member/) + modules.set('child', { ...child, body: 'changed' }) + assert.throws(() => verifyModules(modules, sources, external, read, source => source), /Frozen module changed/) +})