recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +845 -461
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +59 -42
- recce/apis/check_events_api.py +353 -0
- recce/apis/check_func.py +41 -35
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +64 -25
- recce/artifact.py +119 -51
- recce/cli.py +1301 -324
- recce/config.py +43 -34
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404/index.html +2 -0
- recce/data/404.html +2 -1
- recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
- recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
- recce/data/__next.__PAGE__.txt +6 -0
- recce/data/__next._full.txt +32 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +14 -0
- recce/data/__next._tree.txt +8 -0
- recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
- recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
- recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
- recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
- recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
- recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
- recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
- recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
- recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
- recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
- recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
- recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
- recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
- recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
- recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
- recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
- recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
- recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
- recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
- recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
- recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
- recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
- recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
- recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
- recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
- recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
- recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
- recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
- recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
- recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
- recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
- recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
- recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
- recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
- recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
- recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
- recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
- recce/data/_not-found/__next._full.txt +24 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +13 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +6 -0
- recce/data/_not-found/index.html +2 -0
- recce/data/_not-found/index.txt +24 -0
- recce/data/auth_callback.html +68 -0
- recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/checks/__next._full.txt +39 -0
- recce/data/checks/__next._head.txt +8 -0
- recce/data/checks/__next._index.txt +14 -0
- recce/data/checks/__next._tree.txt +8 -0
- recce/data/checks/__next.checks.__PAGE__.txt +10 -0
- recce/data/checks/__next.checks.txt +4 -0
- recce/data/checks/index.html +2 -0
- recce/data/checks/index.txt +39 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +2 -27
- recce/data/index.txt +32 -7
- recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/lineage/__next._full.txt +39 -0
- recce/data/lineage/__next._head.txt +8 -0
- recce/data/lineage/__next._index.txt +14 -0
- recce/data/lineage/__next._tree.txt +8 -0
- recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
- recce/data/lineage/__next.lineage.txt +4 -0
- recce/data/lineage/index.html +2 -0
- recce/data/lineage/index.txt +39 -0
- recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/query/__next._full.txt +37 -0
- recce/data/query/__next._head.txt +8 -0
- recce/data/query/__next._index.txt +14 -0
- recce/data/query/__next._tree.txt +8 -0
- recce/data/query/__next.query.__PAGE__.txt +9 -0
- recce/data/query/__next.query.txt +4 -0
- recce/data/query/index.html +2 -0
- recce/data/query/index.txt +37 -0
- recce/diff.py +6 -12
- recce/event/CONFIG.bak +1 -0
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +725 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +438 -21
- recce/models/run.py +1 -0
- recce/models/types.py +134 -28
- recce/pull_request.py +27 -25
- recce/run.py +179 -122
- recce/server.py +394 -104
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +644 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +196 -149
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +180 -89
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/utils.py +147 -0
- recce/tasks/valuediff.py +247 -155
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +105 -100
- recce/util/cll.py +274 -219
- recce/util/cloud/__init__.py +15 -0
- recce/util/cloud/base.py +115 -0
- recce/util/cloud/check_events.py +190 -0
- recce/util/cloud/checks.py +242 -0
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +347 -72
- recce/util/singleton.py +4 -4
- recce/util/startup_perf.py +121 -0
- recce/yaml/__init__.py +7 -10
- recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
- recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
- recce/state.py +0 -753
- recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
- recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
- recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/conftest.py +0 -13
- tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
- tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
- tests/adapter/dbt_adapter/test_selector.py +0 -177
- tests/tasks/__init__.py +0 -0
- tests/tasks/conftest.py +0 -4
- tests/tasks/test_histogram.py +0 -137
- tests/tasks/test_lineage.py +0 -42
- tests/tasks/test_preset_checks.py +0 -50
- tests/tasks/test_profile.py +0 -73
- tests/tasks/test_query.py +0 -151
- tests/tasks/test_row_count.py +0 -116
- tests/tasks/test_schema.py +0 -99
- tests/tasks/test_top_k.py +0 -73
- tests/tasks/test_valuediff.py +0 -74
- tests/test_cli.py +0 -122
- tests/test_config.py +0 -45
- tests/test_core.py +0 -27
- tests/test_dbt.py +0 -36
- tests/test_pull_request.py +0 -130
- tests/test_server.py +0 -98
- tests/test_state.py +0 -123
- tests/test_summary.py +0 -57
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/config.py
CHANGED
|
@@ -5,11 +5,11 @@ from recce import yaml
|
|
|
5
5
|
from recce.exceptions import RecceConfigException
|
|
6
6
|
from recce.util import SingletonMeta
|
|
7
7
|
|
|
8
|
-
RECCE_CONFIG_FILE =
|
|
9
|
-
RECCE_PRESET_CHECK_COMMENT =
|
|
10
|
-
Please see https://datarecce.io/
|
|
11
|
-
|
|
12
|
-
RECCE_ERROR_LOG_FILE =
|
|
8
|
+
RECCE_CONFIG_FILE = "recce.yml"
|
|
9
|
+
RECCE_PRESET_CHECK_COMMENT = """Preset Checks
|
|
10
|
+
Please see https://docs.datarecce.io/features/preset-checks/
|
|
11
|
+
"""
|
|
12
|
+
RECCE_ERROR_LOG_FILE = "recce_error.log"
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
15
15
|
|
|
@@ -21,83 +21,92 @@ class RecceConfig(metaclass=SingletonMeta):
|
|
|
21
21
|
|
|
22
22
|
def load(self):
|
|
23
23
|
try:
|
|
24
|
-
with open(self.config_file,
|
|
24
|
+
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
25
25
|
config = yaml.safe_load(f)
|
|
26
26
|
self.config = config if config else {}
|
|
27
27
|
self._verify_preset_checks()
|
|
28
28
|
except FileNotFoundError:
|
|
29
|
-
console.print(f
|
|
29
|
+
console.print(f"[[orange3]NOTICE[/orange3]] Generate default Recce config file at '{self.config_file}'")
|
|
30
30
|
self.config = self.generate_template()
|
|
31
31
|
self.save()
|
|
32
32
|
|
|
33
33
|
def _verify_preset_checks(self):
|
|
34
34
|
from recce.tasks.core import CheckValidator
|
|
35
35
|
|
|
36
|
-
if not self.config.get(
|
|
36
|
+
if not self.config.get("checks"):
|
|
37
37
|
return
|
|
38
38
|
|
|
39
|
-
for check in self.config[
|
|
39
|
+
for check in self.config["checks"]:
|
|
40
40
|
try:
|
|
41
|
-
check_type = check.get(
|
|
41
|
+
check_type = check.get("type")
|
|
42
42
|
if check_type is None:
|
|
43
43
|
raise ValueError(f'Check type is required for check "{check}"')
|
|
44
|
-
if check_type ==
|
|
44
|
+
if check_type == "lineage_diff":
|
|
45
45
|
from recce.tasks.lineage import LineageDiffCheckValidator
|
|
46
|
+
|
|
46
47
|
validator = LineageDiffCheckValidator()
|
|
47
|
-
elif check_type ==
|
|
48
|
+
elif check_type == "schema_diff":
|
|
48
49
|
from recce.tasks.schema import SchemaDiffCheckValidator
|
|
50
|
+
|
|
49
51
|
validator = SchemaDiffCheckValidator()
|
|
50
|
-
elif check_type ==
|
|
52
|
+
elif check_type == "row_count_diff":
|
|
51
53
|
from recce.tasks.rowcount import RowCountDiffCheckValidator
|
|
54
|
+
|
|
52
55
|
validator = RowCountDiffCheckValidator()
|
|
53
|
-
elif check_type ==
|
|
56
|
+
elif check_type == "query":
|
|
54
57
|
from recce.tasks.query import QueryCheckValidator
|
|
58
|
+
|
|
55
59
|
validator = QueryCheckValidator()
|
|
56
|
-
elif check_type ==
|
|
60
|
+
elif check_type == "query_diff":
|
|
57
61
|
from recce.tasks.query import QueryDiffCheckValidator
|
|
62
|
+
|
|
58
63
|
validator = QueryDiffCheckValidator()
|
|
59
|
-
elif check_type ==
|
|
64
|
+
elif check_type == "value_diff" or check_type == "value_diff_detail":
|
|
60
65
|
from recce.tasks.valuediff import ValueDiffCheckValidator
|
|
66
|
+
|
|
61
67
|
validator = ValueDiffCheckValidator()
|
|
62
|
-
elif check_type ==
|
|
68
|
+
elif check_type == "profile_diff":
|
|
63
69
|
from recce.tasks.profile import ProfileCheckValidator
|
|
70
|
+
|
|
64
71
|
validator = ProfileCheckValidator()
|
|
65
|
-
elif check_type ==
|
|
72
|
+
elif check_type == "top_k_diff":
|
|
66
73
|
from recce.tasks.top_k import TopKDiffCheckValidator
|
|
74
|
+
|
|
67
75
|
validator = TopKDiffCheckValidator()
|
|
68
|
-
elif check_type ==
|
|
76
|
+
elif check_type == "histogram_diff":
|
|
69
77
|
from recce.tasks.histogram import HistogramDiffCheckValidator
|
|
78
|
+
|
|
70
79
|
validator = HistogramDiffCheckValidator()
|
|
71
80
|
else:
|
|
72
81
|
validator = CheckValidator()
|
|
73
82
|
validator.validate(check)
|
|
74
83
|
except Exception as e:
|
|
75
84
|
import json
|
|
85
|
+
|
|
76
86
|
raise RecceConfigException(
|
|
77
|
-
f"Load preset checks failed from '{self.config_file}'\n{json.dumps(check, indent=2)}",
|
|
78
|
-
|
|
87
|
+
f"Load preset checks failed from '{self.config_file}'\n{json.dumps(check, indent=2)}", cause=e
|
|
88
|
+
)
|
|
79
89
|
|
|
80
90
|
def generate_template(self):
|
|
81
|
-
data = yaml.CommentedMap(
|
|
82
|
-
|
|
83
|
-
data.yaml_set_comment_before_after_key('checks', before=RECCE_PRESET_CHECK_COMMENT)
|
|
91
|
+
data = yaml.CommentedMap(checks=yaml.CommentedSeq())
|
|
92
|
+
data.yaml_set_comment_before_after_key("checks", before=RECCE_PRESET_CHECK_COMMENT)
|
|
84
93
|
# Define default preset checks
|
|
85
94
|
default_checks = [
|
|
86
95
|
yaml.CommentedMap(
|
|
87
|
-
name=
|
|
88
|
-
description=
|
|
89
|
-
type=
|
|
90
|
-
params={
|
|
96
|
+
name="Row count diff",
|
|
97
|
+
description="Check the row count diff for all table models.",
|
|
98
|
+
type="row_count_diff",
|
|
99
|
+
params={"select": "state:modified,config.materialized:table"},
|
|
91
100
|
),
|
|
92
101
|
yaml.CommentedMap(
|
|
93
|
-
name=
|
|
94
|
-
description=
|
|
95
|
-
type=
|
|
96
|
-
)
|
|
102
|
+
name="Schema diff",
|
|
103
|
+
description="Check the schema diff for all nodes.",
|
|
104
|
+
type="schema_diff",
|
|
105
|
+
),
|
|
97
106
|
]
|
|
98
107
|
|
|
99
108
|
for check in default_checks:
|
|
100
|
-
data[
|
|
109
|
+
data["checks"].append(check)
|
|
101
110
|
|
|
102
111
|
return data
|
|
103
112
|
|
|
@@ -108,7 +117,7 @@ class RecceConfig(metaclass=SingletonMeta):
|
|
|
108
117
|
self.config[key] = value
|
|
109
118
|
|
|
110
119
|
def save(self):
|
|
111
|
-
with open(RECCE_CONFIG_FILE,
|
|
120
|
+
with open(RECCE_CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
112
121
|
yaml.dump(self.config, f)
|
|
113
122
|
|
|
114
123
|
def __str__(self):
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os.path
|
|
3
|
+
import random
|
|
4
|
+
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from urllib.parse import parse_qs, urlparse
|
|
9
|
+
|
|
10
|
+
from cryptography.hazmat.backends import default_backend
|
|
11
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from recce.event import update_recce_api_token
|
|
17
|
+
from recce.exceptions import RecceConfigException
|
|
18
|
+
from recce.util.onboarding_state import update_onboarding_state
|
|
19
|
+
from recce.util.recce_cloud import RECCE_CLOUD_BASE_URL, RecceCloud
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
24
|
+
_server_lock = threading.Lock()
|
|
25
|
+
_connection_url = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decrypt_code(private_key: RSAPrivateKey, code: str) -> str:
|
|
29
|
+
ciphertext = base64.b64decode(code)
|
|
30
|
+
plaintext = private_key.decrypt(
|
|
31
|
+
ciphertext,
|
|
32
|
+
padding.OAEP(
|
|
33
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()), # Node.js uses SHA1 by default
|
|
34
|
+
algorithm=hashes.SHA1(),
|
|
35
|
+
label=None,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
return plaintext.decode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle_callback_request(query_string: str, private_key: RSAPrivateKey):
|
|
42
|
+
query_params = parse_qs(query_string)
|
|
43
|
+
code = query_params.get("code", [None])[0]
|
|
44
|
+
if not code:
|
|
45
|
+
raise RecceConfigException("Missing `code` in query")
|
|
46
|
+
|
|
47
|
+
api_token = decrypt_code(private_key, code)
|
|
48
|
+
if not RecceCloud(api_token).verify_token():
|
|
49
|
+
raise RecceConfigException("Invalid Recce Cloud API token")
|
|
50
|
+
|
|
51
|
+
update_recce_api_token(api_token)
|
|
52
|
+
update_onboarding_state(api_token, False)
|
|
53
|
+
|
|
54
|
+
return api_token # for testability/debugging
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_callback_handler(private_key: RSAPrivateKey):
|
|
58
|
+
class OneTimeHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
59
|
+
def do_GET(self):
|
|
60
|
+
try:
|
|
61
|
+
with open(os.path.join(static_folder_path, "auth_callback.html"), "r", encoding="utf-8") as f:
|
|
62
|
+
callback_html_content = f.read()
|
|
63
|
+
|
|
64
|
+
# Parse query parameters
|
|
65
|
+
parsed_url = urlparse(self.path)
|
|
66
|
+
|
|
67
|
+
handle_callback_request(parsed_url.query, private_key)
|
|
68
|
+
|
|
69
|
+
# Construct HTML content
|
|
70
|
+
self.send_response(200)
|
|
71
|
+
self.send_header("Content-Type", "text/html")
|
|
72
|
+
self.send_header("Content-Length", str(len(callback_html_content.encode())))
|
|
73
|
+
self.end_headers()
|
|
74
|
+
self.wfile.write(callback_html_content.encode())
|
|
75
|
+
|
|
76
|
+
except Exception:
|
|
77
|
+
console.print_exception()
|
|
78
|
+
self.send_response(500)
|
|
79
|
+
self.end_headers()
|
|
80
|
+
self.wfile.write(b"<h1>Internal Server Error</h1>")
|
|
81
|
+
finally:
|
|
82
|
+
# Shut down the server after handling the first request
|
|
83
|
+
# Shutdown in a new thread to avoid deadlock
|
|
84
|
+
self.server.server_close()
|
|
85
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
86
|
+
|
|
87
|
+
def log_message(self, format, *args):
|
|
88
|
+
# Suppress default logging
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
return OneTimeHTTPRequestHandler
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_callback_server_running():
|
|
95
|
+
return _server_lock.locked()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_connection_url():
|
|
99
|
+
return _connection_url
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_one_time_http_server(private_key: RSAPrivateKey, port=8080):
|
|
103
|
+
handler = make_callback_handler(private_key)
|
|
104
|
+
server = HTTPServer(("localhost", port), handler)
|
|
105
|
+
server.serve_forever()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def prepare_connection_url(public_key: RSAPublicKey):
|
|
109
|
+
public_key_pem_bytes = public_key.public_bytes(
|
|
110
|
+
encoding=serialization.Encoding.PEM,
|
|
111
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
112
|
+
)
|
|
113
|
+
public_key_pem_str = base64.b64encode(public_key_pem_bytes).decode("utf-8")
|
|
114
|
+
callback_port = random.randint(10000, 15000)
|
|
115
|
+
connect_url = f"{RECCE_CLOUD_BASE_URL}/connect?_key={public_key_pem_str}&_port={callback_port}"
|
|
116
|
+
return connect_url, callback_port
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def generate_key_pair() -> Tuple[RSAPrivateKey, RSAPublicKey]:
|
|
120
|
+
key_size = 2048 # Should be at least 2048
|
|
121
|
+
|
|
122
|
+
private_key = rsa.generate_private_key(
|
|
123
|
+
public_exponent=65537, key_size=key_size, backend=default_backend() # Do not change
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
public_key = private_key.public_key()
|
|
127
|
+
return private_key, public_key
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def connect_to_cloud_background_task(private_key: RSAPrivateKey, callback_port, connection_url):
|
|
131
|
+
if is_callback_server_running():
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
with _server_lock:
|
|
135
|
+
global _connection_url
|
|
136
|
+
_connection_url = connection_url
|
|
137
|
+
run_one_time_http_server(private_key, callback_port)
|
|
138
|
+
_connection_url = None
|
recce/core.py
CHANGED
|
@@ -3,15 +3,21 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Callable, Dict, Optional,
|
|
6
|
+
from typing import Callable, Dict, List, Optional, Set, Tuple
|
|
7
7
|
|
|
8
8
|
from recce.adapter.base import BaseAdapter
|
|
9
9
|
from recce.models import Check, Run
|
|
10
10
|
from recce.models.types import LineageDiff
|
|
11
|
-
from recce.state import
|
|
11
|
+
from recce.state import (
|
|
12
|
+
GitRepoInfo,
|
|
13
|
+
PullRequestInfo,
|
|
14
|
+
RecceState,
|
|
15
|
+
RecceStateLoader,
|
|
16
|
+
RecceStateMetadata,
|
|
17
|
+
)
|
|
12
18
|
from recce.util.recce_cloud import set_recce_cloud_onboarding_state
|
|
13
19
|
|
|
14
|
-
logger = logging.getLogger(
|
|
20
|
+
logger = logging.getLogger("uvicorn")
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
@dataclass
|
|
@@ -25,8 +31,8 @@ class RecceContext:
|
|
|
25
31
|
|
|
26
32
|
@classmethod
|
|
27
33
|
def load(cls, **kwargs):
|
|
28
|
-
state_loader: RecceStateLoader = kwargs.get(
|
|
29
|
-
is_review_mode = kwargs.get(
|
|
34
|
+
state_loader: RecceStateLoader = kwargs.get("state_loader")
|
|
35
|
+
is_review_mode = kwargs.get("review", False)
|
|
30
36
|
|
|
31
37
|
context = cls(
|
|
32
38
|
review_mode=is_review_mode,
|
|
@@ -34,14 +40,16 @@ class RecceContext:
|
|
|
34
40
|
)
|
|
35
41
|
|
|
36
42
|
# Initiate the adapter
|
|
37
|
-
if kwargs.get(
|
|
38
|
-
logger.warning(
|
|
43
|
+
if kwargs.get("sqlmesh", False):
|
|
44
|
+
logger.warning("SQLMesh adapter is still in EXPERIMENTAL mode.")
|
|
39
45
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
40
|
-
|
|
46
|
+
|
|
47
|
+
context.adapter_type = "sqlmesh"
|
|
41
48
|
context.adapter = SqlmeshAdapter.load(**kwargs)
|
|
42
49
|
else:
|
|
43
50
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
44
|
-
|
|
51
|
+
|
|
52
|
+
context.adapter_type = "dbt"
|
|
45
53
|
context.adapter = DbtAdapter.load(**kwargs)
|
|
46
54
|
|
|
47
55
|
# Import state
|
|
@@ -52,7 +60,7 @@ class RecceContext:
|
|
|
52
60
|
|
|
53
61
|
if is_review_mode:
|
|
54
62
|
if not state:
|
|
55
|
-
raise Exception(
|
|
63
|
+
raise Exception("The state file is required for review mode")
|
|
56
64
|
|
|
57
65
|
return context
|
|
58
66
|
|
|
@@ -76,14 +84,14 @@ class RecceContext:
|
|
|
76
84
|
curr = self.get_lineage(base=False)
|
|
77
85
|
base = self.get_lineage(base=True)
|
|
78
86
|
|
|
79
|
-
for unique_id, node in curr[
|
|
80
|
-
if excluded_types and node.get(
|
|
87
|
+
for unique_id, node in curr["nodes"].items():
|
|
88
|
+
if excluded_types and node.get("resource_type") in excluded_types:
|
|
81
89
|
continue
|
|
82
|
-
name_to_unique_id[node[
|
|
83
|
-
for unique_id, node in base[
|
|
84
|
-
if excluded_types and node.get(
|
|
90
|
+
name_to_unique_id[node["name"]] = unique_id
|
|
91
|
+
for unique_id, node in base["nodes"].items():
|
|
92
|
+
if excluded_types and node.get("resource_type") in excluded_types:
|
|
85
93
|
continue
|
|
86
|
-
name_to_unique_id[node[
|
|
94
|
+
name_to_unique_id[node["name"]] = unique_id
|
|
87
95
|
return name_to_unique_id
|
|
88
96
|
|
|
89
97
|
def start_monitor_artifacts(self, callback: Callable = None):
|
|
@@ -118,7 +126,7 @@ class RecceContext:
|
|
|
118
126
|
state.git = self.state_loader.state.git
|
|
119
127
|
state.pull_request = self.state_loader.state.pull_request
|
|
120
128
|
else:
|
|
121
|
-
git = GitRepoInfo.
|
|
129
|
+
git = GitRepoInfo.from_current_repository()
|
|
122
130
|
if git:
|
|
123
131
|
state.git = git
|
|
124
132
|
if self.state_loader.pr_info:
|
|
@@ -137,10 +145,10 @@ class RecceContext:
|
|
|
137
145
|
state.runs = self.runs
|
|
138
146
|
state.checks = self.checks
|
|
139
147
|
state.artifacts = self.adapter.export_artifacts()
|
|
140
|
-
git = GitRepoInfo.
|
|
148
|
+
git = GitRepoInfo.from_current_repository()
|
|
141
149
|
if git:
|
|
142
150
|
state.git = git
|
|
143
|
-
pr = PullRequestInfo(url=os.getenv(
|
|
151
|
+
pr = PullRequestInfo(url=os.getenv("RECCE_PR_URL"))
|
|
144
152
|
state.pull_request = pr
|
|
145
153
|
|
|
146
154
|
return state
|
|
@@ -152,38 +160,37 @@ class RecceContext:
|
|
|
152
160
|
:param method: merge, revert, overwrite
|
|
153
161
|
|
|
154
162
|
"""
|
|
155
|
-
if method ==
|
|
163
|
+
if method == "merge":
|
|
156
164
|
self.state_loader.refresh()
|
|
157
165
|
self.import_state(self.state_loader.state, merge=True)
|
|
158
166
|
state = self.export_state()
|
|
159
167
|
self.state_loader.export(state)
|
|
160
|
-
elif method ==
|
|
168
|
+
elif method == "revert":
|
|
161
169
|
self.state_loader.refresh()
|
|
162
170
|
self.import_state(self.state_loader.state, merge=False)
|
|
163
|
-
elif method ==
|
|
171
|
+
elif method == "overwrite":
|
|
164
172
|
state = self.export_state()
|
|
165
173
|
self.state_loader.export(state)
|
|
166
174
|
else:
|
|
167
|
-
raise Exception(f
|
|
175
|
+
raise Exception(f"Unsupported method: {method}")
|
|
168
176
|
|
|
169
177
|
def _merge_checks(self, import_checks: list[Check]):
|
|
170
178
|
checks = list(self.checks)
|
|
171
179
|
imports = 0
|
|
172
180
|
|
|
173
181
|
def _calculate_checksum(c: Check):
|
|
174
|
-
payload = json.dumps(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
payload = json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"type": str(c.type),
|
|
185
|
+
"params": c.params,
|
|
186
|
+
"view_options": c.view_options,
|
|
187
|
+
},
|
|
188
|
+
sort_keys=True,
|
|
189
|
+
)
|
|
179
190
|
return hashlib.sha256(payload.encode()).hexdigest()
|
|
180
191
|
|
|
181
|
-
checksum_map = {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
check_map = {
|
|
185
|
-
c.check_id: c for c in self.checks
|
|
186
|
-
}
|
|
192
|
+
checksum_map = {_calculate_checksum(c): c for c in self.checks if c.is_preset}
|
|
193
|
+
check_map = {c.check_id: c for c in self.checks}
|
|
187
194
|
|
|
188
195
|
# merge checks
|
|
189
196
|
for imported in import_checks:
|
|
@@ -219,12 +226,12 @@ class RecceContext:
|
|
|
219
226
|
return imports
|
|
220
227
|
|
|
221
228
|
def import_state(self, import_state: RecceState, merge: bool = True):
|
|
222
|
-
|
|
229
|
+
"""
|
|
223
230
|
Import the state from another RecceState object.
|
|
224
231
|
|
|
225
232
|
:param import_state: the state to import
|
|
226
233
|
:param merge: whether to merge the state or replace the current state
|
|
227
|
-
|
|
234
|
+
"""
|
|
228
235
|
import_runs = 0
|
|
229
236
|
import_checks = 0
|
|
230
237
|
if merge:
|
|
@@ -261,24 +268,25 @@ class RecceContext:
|
|
|
261
268
|
def mark_onboarding_completed(self):
|
|
262
269
|
if self.state_loader.cloud_mode:
|
|
263
270
|
try:
|
|
264
|
-
token = self.state_loader.cloud_options.get(
|
|
265
|
-
set_recce_cloud_onboarding_state(token,
|
|
271
|
+
token = self.state_loader.cloud_options.get("github_token")
|
|
272
|
+
set_recce_cloud_onboarding_state(token, "completed")
|
|
266
273
|
except Exception as e:
|
|
267
|
-
logger.debug(f
|
|
274
|
+
logger.debug(f"Failed to mark onboarding completed in Recce Cloud. Reason: {str(e)}")
|
|
268
275
|
else:
|
|
269
276
|
# Skip the onboarding state for non-cloud mode
|
|
270
277
|
pass
|
|
271
278
|
|
|
272
279
|
@staticmethod
|
|
273
280
|
def verify_required_artifacts(**kwargs) -> Tuple[bool, Optional[str]]:
|
|
274
|
-
if kwargs.get(
|
|
281
|
+
if kwargs.get("sqlmesh", False):
|
|
275
282
|
pass
|
|
276
283
|
else:
|
|
277
284
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
285
|
+
|
|
278
286
|
try:
|
|
279
287
|
DbtAdapter.load(**kwargs)
|
|
280
288
|
except FileNotFoundError as e:
|
|
281
|
-
return False, f"Cannot load the manifest: '{e.filename}'"
|
|
289
|
+
return False, f"Cannot load the manifest: '{e.filename}'. Type 'recce debug'."
|
|
282
290
|
|
|
283
291
|
return True, None
|
|
284
292
|
|
|
@@ -289,18 +297,18 @@ class RecceContext:
|
|
|
289
297
|
"""
|
|
290
298
|
The state loader mode is used for telemetry purpose.
|
|
291
299
|
"""
|
|
292
|
-
if os.environ.get(
|
|
293
|
-
return
|
|
300
|
+
if os.environ.get("DEMO", False):
|
|
301
|
+
return "demo"
|
|
294
302
|
|
|
295
303
|
if not self.state_loader:
|
|
296
|
-
return
|
|
304
|
+
return "none"
|
|
297
305
|
|
|
298
306
|
if self.state_loader.cloud_mode:
|
|
299
|
-
return
|
|
307
|
+
return "cloud"
|
|
300
308
|
elif self.state_loader.state_file:
|
|
301
|
-
return
|
|
309
|
+
return "file"
|
|
302
310
|
else:
|
|
303
|
-
return
|
|
311
|
+
return "none"
|
|
304
312
|
|
|
305
313
|
|
|
306
314
|
recce_context: Optional[RecceContext] = None
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<!DOCTYPE html><!--nX_Uz0AH6Tc6hIQUFGqaB--><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="/logo/recce-logo-white.png"/><link rel="stylesheet" href="/_next/static/chunks/3a9f021f38eb5574.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/chunks/859462b0858aef88.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/chunks/6d8557f062aa4386.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/chunks/d0f91117d77ff844.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/chunks/923964f18c87d0f1.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/69e4f06ccfdfc3ac.js"/><script src="/_next/static/chunks/0ce56d67ef5779ca.js" async=""></script><script src="/_next/static/chunks/d6c8667911c2500f.js" async=""></script><script src="/_next/static/chunks/2f016dc4a3edad2e.js" async=""></script><script src="/_next/static/chunks/turbopack-1fad664f62979b93.js" async=""></script><script src="/_next/static/chunks/7fbe3650bd83b6b5.js" async=""></script><script src="/_next/static/chunks/4599182bffb64661.js" async=""></script><script src="/_next/static/chunks/83fa823a825674f6.js" async=""></script><script src="/_next/static/chunks/9fed8b4b2b924054.js" async=""></script><script src="/_next/static/chunks/2c357efc34c5b859.js" async=""></script><script src="/_next/static/chunks/dc074049c9d12d97.js" async=""></script><script src="/_next/static/chunks/40079da8d2b8f651.js" async=""></script><script src="/_next/static/chunks/c734f9ad957de0b4.js" async=""></script><script src="/_next/static/chunks/2e9d95d2d48c479c.js" async=""></script><script src="/_next/static/chunks/848a6c9b5f55f7ed.js" async=""></script><script src="/_next/static/chunks/b6949f6c5892110c.js" async=""></script><link rel="preload" href="https://www.googletagmanager.com/gtm.js?id=GTM-M2HVJQFD" as="script"/><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>recce</title><meta name="description" content="Recce: Data validation toolkit for comprehensive PR review"/><link rel="icon" href="/favicon.ico?favicon.a8d38d84.ico" sizes="32x32" type="image/x-icon"/><script src="/_next/static/chunks/a6dad97d9634a72d.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><style data-emotion="css-global 1sm4dhk">html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box;-webkit-text-size-adjust:100%;}*,*::before,*::after{box-sizing:inherit;}strong,b{font-weight:700;}body{margin:0;color:#171717;font-size:0.875rem;line-height:1.5;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;background-color:#FFFFFF;}@media print{body{background-color:#fff;}}body::backdrop{background-color:#FFFFFF;}</style><style data-emotion="css lb61rc">.css-lb61rc{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;height:100vh;overflow:hidden;}</style><div class="MuiBox-root css-lb61rc"><style data-emotion="css 19a3m4u">.css-19a3m4u{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:10px;min-height:40px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:rgb(255, 110, 66);}</style><div class="MuiBox-root css-19a3m4u"><style data-emotion="css 1cto74x">.css-1cto74x{-webkit-text-decoration:none;text-decoration:none;color:#2A6CA7;text-decoration-color:#79B0E2;}.css-1cto74x:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-1cto74x:hover{color:#225581;}.css-1cto74x:hover{-webkit-text-decoration:none;text-decoration:none;}</style><style data-emotion="css oqdijk">.css-oqdijk{margin:0;font:inherit;line-height:inherit;letter-spacing:inherit;color:#3182CE;font-weight:500;-webkit-text-decoration:none;text-decoration:none;color:#2A6CA7;text-decoration-color:#79B0E2;}.css-oqdijk:hover{-webkit-text-decoration:underline;text-decoration:underline;}.css-oqdijk:hover{color:#225581;}.css-oqdijk:hover{-webkit-text-decoration:none;text-decoration:none;}</style><a class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-oqdijk" href="https://reccehq.com/" target="_blank"><style data-emotion="css 15zum9u">.css-15zum9u{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:10px;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}</style><div class="MuiBox-root css-15zum9u"><style data-emotion="css u2at6n">.css-u2at6n{width:20px;height:20px;margin-left:18px;}</style><img class="MuiBox-root css-u2at6n" src="/logo/recce-logo-white.png" alt="recce-logo-white"/><style data-emotion="css 1f16f3g">.css-1f16f3g{margin:0;font-weight:600;font-size:1.25rem;line-height:1.4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";color:white;font-family:"Montserrat",sans-serif;font-size:1.25rem;}</style><h4 class="MuiTypography-root MuiTypography-h4 css-1f16f3g">RECCE</h4></div></a><style data-emotion="css 1jcz15f">.css-1jcz15f{margin:0;font-size:0.875rem;line-height:1.5;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;color:rgba(255,255,255,0.8);font-size:sm;text-transform:uppercase;border-width:1px;padding-left:4px;padding-right:4px;border-radius:4.5px;}</style><span class="MuiTypography-root MuiTypography-body1 css-1jcz15f"></span><style data-emotion="css 1rr4qq7">.css-1rr4qq7{-webkit-flex:1;-ms-flex:1;flex:1;}</style><div class="MuiBox-root css-1rr4qq7"></div><style data-emotion="css 1824a4q">.css-1824a4q{color:white;font-size:0.875rem;font-weight:600;background-color:brand.700;border-radius:6px;padding-left:12px;padding-right:12px;padding-top:4px;padding-bottom:4px;margin-right:8px;cursor:pointer;border:none;}</style><button class="MuiBox-root css-1824a4q">Connect to Cloud</button></div><style data-emotion="css 1dhtcsx">.css-1dhtcsx{border-bottom:1px solid lightgray;padding-left:12px;padding-right:12px;}</style><div class="MuiBox-root css-1dhtcsx"><style data-emotion="css kkbn2n">.css-kkbn2n{display:grid;grid-template-columns:1fr auto 1fr;width:100%;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}</style><div class="MuiBox-root css-kkbn2n"><style data-emotion="css 1qlx8uq">.css-1qlx8uq{overflow:hidden;min-height:48px;-webkit-overflow-scrolling:touch;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;border-bottom:none;min-height:auto;}@media (max-width:599.95px){.css-1qlx8uq .MuiTabs-scrollButtons{display:none;}}</style><div class="MuiTabs-root css-1qlx8uq"><style data-emotion="css 2xu61f">.css-2xu61f{position:relative;display:inline-block;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;white-space:nowrap;overflow-x:hidden;width:100%;}</style><div style="overflow:hidden;margin-bottom:0" class="MuiTabs-scroller MuiTabs-fixed css-2xu61f"><style data-emotion="css 17do188">.css-17do188{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}</style><div role="tablist" class="MuiTabs-list MuiTabs-flexContainer css-17do188"><style data-emotion="css 1n2jfwv">.css-1n2jfwv{text-transform:none;font-weight:500;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:0.875rem;line-height:1.25;max-width:360px;min-width:90px;position:relative;min-height:48px;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:12px 16px;overflow:hidden;white-space:normal;text-align:center;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;color:#525252;text-transform:none;font-weight:500;min-height:48px;padding:0px;}.css-1n2jfwv.Mui-selected{color:#3182CE;}.css-1n2jfwv.Mui-disabled{color:rgba(0, 0, 0, 0.38);}</style><style data-emotion="css 1ameuk9">.css-1ameuk9{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;position:relative;box-sizing:border-box;-webkit-tap-highlight-color:transparent;background-color:transparent;outline:0;border:0;margin:0;border-radius:0;padding:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-moz-appearance:none;-webkit-appearance:none;-webkit-text-decoration:none;text-decoration:none;color:inherit;text-transform:none;font-weight:500;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:0.875rem;line-height:1.25;max-width:360px;min-width:90px;position:relative;min-height:48px;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:12px 16px;overflow:hidden;white-space:normal;text-align:center;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;color:#525252;text-transform:none;font-weight:500;min-height:48px;padding:0px;}.css-1ameuk9::-moz-focus-inner{border-style:none;}.css-1ameuk9.Mui-disabled{pointer-events:none;cursor:default;}@media print{.css-1ameuk9{-webkit-print-color-adjust:exact;color-adjust:exact;}}.css-1ameuk9.Mui-selected{color:#3182CE;}.css-1ameuk9.Mui-disabled{color:rgba(0, 0, 0, 0.38);}</style><button class="MuiButtonBase-root Mui-disabled MuiTab-root MuiTab-textColorPrimary Mui-selected Mui-disabled css-1ameuk9" tabindex="-1" type="button" disabled="" role="tab" aria-selected="true"><style data-emotion="css 171onha">.css-171onha{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px;}</style><div class="MuiBox-root css-171onha"><a style="text-decoration:none;color:inherit;padding:0.875rem 1.1875rem" href="/lineage/">Lineage</a></div><style data-emotion="css l6r5ej">.css-l6r5ej{position:absolute;height:2px;bottom:0;width:100%;-webkit-transition:all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;transition:all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;background-color:#3182CE;height:2px;}</style><span class="MuiTabs-indicator css-l6r5ej"></span></button><button class="MuiButtonBase-root Mui-disabled MuiTab-root MuiTab-textColorPrimary Mui-disabled css-1ameuk9" tabindex="-1" type="button" disabled="" role="tab" aria-selected="false"><div class="MuiBox-root css-171onha"><a style="text-decoration:none;color:inherit;padding:0.875rem 1.1875rem" href="/query/">Query</a></div></button><button class="MuiButtonBase-root Mui-disabled MuiTab-root MuiTab-textColorPrimary Mui-disabled css-1ameuk9" tabindex="-1" type="button" disabled="" role="tab" aria-selected="false"><div class="MuiBox-root css-171onha"><a style="text-decoration:none;color:inherit;padding:0.875rem 1.1875rem;display:flex;gap:3px;align-items:center" href="/checks/">Checklist<!-- --> </a></div></button></div></div></div><style data-emotion="css vx1bta">.css-vx1bta{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:12px;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;}</style><div class="MuiBox-root css-vx1bta"></div></div></div><div style="display:flex;flex-direction:row;height:100%"><div class="MuiBox-root css-0" style="contain:size"></div><div style="display:flex;flex-direction:column;flex:1;contain:size"><!--$?--><template id="B:0"></template><style data-emotion="css 11iw7t1">.css-11iw7t1{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:100%;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;contain:size;}</style><div class="MuiBox-root css-11iw7t1"><style data-emotion="css tkevr6">.css-tkevr6{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;height:100%;}</style><div class="MuiBox-root css-tkevr6"><style data-emotion="css 18h6s44 animation-61bdi0">.css-18h6s44{display:inline-block;-webkit-animation:animation-61bdi0 1.4s linear infinite;animation:animation-61bdi0 1.4s linear infinite;color:#3182CE;}@-webkit-keyframes animation-61bdi0{0%{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}@keyframes animation-61bdi0{0%{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}</style><span class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorPrimary css-18h6s44" style="width:48px;height:48px" role="progressbar"><style data-emotion="css 4ejps8">.css-4ejps8{display:block;}</style><svg class="MuiCircularProgress-svg css-4ejps8" viewBox="22 22 44 44"><style data-emotion="css 13odlrs animation-1o38n3e">.css-13odlrs{stroke:currentColor;stroke-dasharray:80px,200px;stroke-dashoffset:0;-webkit-animation:animation-1o38n3e 1.4s ease-in-out infinite;animation:animation-1o38n3e 1.4s ease-in-out infinite;}@-webkit-keyframes animation-1o38n3e{0%{stroke-dasharray:1px,200px;stroke-dashoffset:0;}50%{stroke-dasharray:100px,200px;stroke-dashoffset:-15px;}100%{stroke-dasharray:1px,200px;stroke-dashoffset:-126px;}}@keyframes animation-1o38n3e{0%{stroke-dasharray:1px,200px;stroke-dashoffset:0;}50%{stroke-dasharray:100px,200px;stroke-dashoffset:-15px;}100%{stroke-dasharray:1px,200px;stroke-dashoffset:-126px;}}</style><circle class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-13odlrs" cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6"></circle></svg></span></div></div><!--/$--><style data-emotion="css 10klw3m">.css-10klw3m{height:100%;}</style><div class="MuiBox-root css-10klw3m"></div></div></div></div><style data-emotion="css depviu">@media print{.css-depviu{position:absolute!important;}}</style><script>requestAnimationFrame(function(){$RT=performance.now()});</script><script src="/_next/static/chunks/69e4f06ccfdfc3ac.js" id="_R_" async=""></script><div hidden id="S:0"><style data-emotion="css 1arxj4v">.css-1arxj4v{padding:0px;contain:content;}</style><div class="MuiBox-root css-1arxj4v"><style data-emotion="css r2yx1p">.css-r2yx1p{display:none;height:100%;position:absolute;inset:0;}</style><div class="MuiBox-root css-r2yx1p"></div><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></div></div><script>$RB=[];$RV=function(a){$RT=performance.now();for(var b=0;b<a.length;b+=2){var c=a[b],e=a[b+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&requestAnimationFrame(g._reactRetry)}}a.length=0};
|
|
2
|
+
$RC=function(a,b){if(b=document.getElementById(b))(a=document.getElementById(a))?(a.previousSibling.data="$~",$RB.push(a,b),2===$RB.length&&("number"!==typeof $RT?requestAnimationFrame($RV.bind(null,$RB)):(a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:$RT+300-a)))):b.parentNode.removeChild(b)};$RC("B:0","S:0")</script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[634135,[\"/_next/static/chunks/7fbe3650bd83b6b5.js\",\"/_next/static/chunks/4599182bffb64661.js\",\"/_next/static/chunks/83fa823a825674f6.js\",\"/_next/static/chunks/9fed8b4b2b924054.js\",\"/_next/static/chunks/2c357efc34c5b859.js\",\"/_next/static/chunks/dc074049c9d12d97.js\",\"/_next/static/chunks/40079da8d2b8f651.js\"],\"GoogleTagManager\"]\n3:I[485153,[\"/_next/static/chunks/7fbe3650bd83b6b5.js\",\"/_next/static/chunks/4599182bffb64661.js\",\"/_next/static/chunks/83fa823a825674f6.js\",\"/_next/static/chunks/9fed8b4b2b924054.js\",\"/_next/static/chunks/2c357efc34c5b859.js\",\"/_next/static/chunks/dc074049c9d12d97.js\",\"/_next/static/chunks/40079da8d2b8f651.js\"],\"\"]\n4:I[84654,[\"/_next/static/chunks/7fbe3650bd83b6b5.js\",\"/_next/static/chunks/4599182bffb64661.js\",\"/_next/static/chunks/83fa823a825674f6.js\",\"/_next/static/chunks/9fed8b4b2b924054.js\",\"/_next/static/chunks/2c357efc34c5b859.js\",\"/_next/static/chunks/dc074049c9d12d97.js\",\"/_next/static/chunks/40079da8d2b8f651.js\"],\"default\"]\n5:I[302240,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"default\"]\n6:I[408821,[\"/_next/static/chunks/7fbe3650bd83b6b5.js\",\"/_next/static/chunks/4599182bffb64661.js\",\"/_next/static/chunks/83fa823a825674f6.js\",\"/_next/static/chunks/9fed8b4b2b924054.js\",\"/_next/static/chunks/2c357efc34c5b859.js\",\"/_next/static/chunks/dc074049c9d12d97.js\",\"/_next/static/chunks/40079da8d2b8f651.js\",\"/_next/static/chunks/848a6c9b5f55f7ed.js\"],\"default\"]\n7:I[354876,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"default\"]\n8:I[893065,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"OutletBoundary\"]\n9:\"$Sreact.suspense\"\nd:I[253348,[\"/_next/static/chunks/b6949f6c5892110c.js\",\"/_next/static/chunks/40079da8d2b8f651.js\"],\"default\"]\n:HL[\"/_next/static/chunks/3a9f021f38eb5574.css\",\"style\"]\n:HL[\"/_next/static/chunks/859462b0858aef88.css\",\"style\"]\n:HL[\"/_next/static/chunks/6d8557f062aa4386.css\",\"style\"]\n:HL[\"/_next/static/chunks/d0f91117d77ff844.css\",\"style\"]\n:HL[\"/_next/static/chunks/923964f18c87d0f1.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"nX-Uz0AH6Tc6hIQUFGqaB\",\"c\":[\"\",\"_not-found\",\"\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/3a9f021f38eb5574.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/859462b0858aef88.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"2\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/6d8557f062aa4386.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"3\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/d0f91117d77ff844.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"4\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/923964f18c87d0f1.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/7fbe3650bd83b6b5.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/4599182bffb64661.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-2\",{\"src\":\"/_next/static/chunks/83fa823a825674f6.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-3\",{\"src\":\"/_next/static/chunks/9fed8b4b2b924054.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-4\",{\"src\":\"/_next/static/chunks/2c357efc34c5b859.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-5\",{\"src\":\"/_next/static/chunks/dc074049c9d12d97.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-6\",{\"src\":\"/_next/static/chunks/40079da8d2b8f651.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"$L2\",null,{\"gtmId\":\"GTM-M2HVJQFD\"}],[\"$\",\"$L3\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"\\n (function() {\\n const hash = window.location.hash;\\n if (hash.startsWith('#!')) {\\n const newLocation = window.location.origin + window.location.pathname;\\n window.location.assign(newLocation);\\n }\\n })();\\n \"}}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L4\",null,{\"lineage\":\"$undefined\",\"children\":[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$6\",\"errorStyles\":[],\"errorScripts\":[[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/848a6c9b5f55f7ed.js\",\"async\":true}]],\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:2:props:children:props:children:props:notFound:0:1:props:style\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$0:f:0:1:0:props:children:1:props:children:2:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style\",\"children\":404}],[\"$\",\"div\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:2:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$0:f:0:1:0:props:children:1:props:children:2:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style\",\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L8\",null,{\"children\":[\"$\",\"$9\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@a\"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$Lb\",\"$Lc\",null]}],false]],\"m\":\"$undefined\",\"G\":[\"$d\",[]],\"S\":true}\n"])</script><script>self.__next_f.push([1,"e:I[893065,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"ViewportBoundary\"]\n10:I[893065,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"MetadataBoundary\"]\nb:[\"$\",\"$Le\",null,{\"children\":\"$@f\"}]\nc:[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$L10\",null,{\"children\":[\"$\",\"$9\",null,{\"name\":\"Next.Metadata\",\"children\":\"$@11\"}]}]}]\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"12:I[666314,[\"/_next/static/chunks/c734f9ad957de0b4.js\",\"/_next/static/chunks/2e9d95d2d48c479c.js\"],\"IconMark\"]\n11:[[\"$\",\"title\",\"0\",{\"children\":\"recce\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Recce: Data validation toolkit for comprehensive PR review\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.a8d38d84.ico\",\"sizes\":\"32x32\",\"type\":\"image/x-icon\"}],[\"$\",\"$L12\",\"3\",{}]]\na:null\n"])</script></body></html>
|