loki-mode 7.129.5 → 8.0.1

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.
Files changed (75) hide show
  1. package/README.md +121 -9
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +326 -92
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +169 -105
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +141 -0
  29. package/autonomy/loki +196 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4587 -671
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/providers.md +42 -0
  75. package/skills/quality-gates.md +22 -0
@@ -0,0 +1,793 @@
1
+ #!/usr/bin/env python3
2
+ """Detect rendered mock operational data with collection-level binding."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Iterable
13
+
14
+
15
+ RESOLUTION_EXTENSIONS = (
16
+ ".ts", ".tsx", ".mts", ".js", ".jsx", ".mjs", ".cts", ".cjs", ".vue", ".svelte",
17
+ )
18
+ SOURCE_EXTENSIONS = frozenset(RESOLUTION_EXTENSIONS)
19
+ SOURCE_INDEX_EXCLUDE = re.compile(
20
+ r"(^|/)(node_modules|\.git|\.loki|\.next|\.cache|dist|build|out|coverage|"
21
+ r"storybook)(/|$)|\.d\.(?:ts|mts|cts)$",
22
+ re.IGNORECASE,
23
+ )
24
+ RENDER_EXCLUDE = re.compile(
25
+ r"(^|/)(node_modules|\.git|\.loki|\.next|\.cache|dist|build|out|coverage|"
26
+ r"__mocks__|__tests__|__fixtures__|fixtures?|mocks?|stories|storybook)(/|$)"
27
+ r"|\.(test|spec|stories|story|mock|fixture)\."
28
+ r"|\.d\.(?:ts|mts|cts)$|(^|/)msw|(^|/)setup",
29
+ re.IGNORECASE,
30
+ )
31
+ DECLARATION = re.compile(
32
+ r"\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*"
33
+ r"(?::[^=;]+)?=\s*",
34
+ re.MULTILINE,
35
+ )
36
+ NAMED_IMPORT = re.compile(
37
+ r"\bimport\s*\{([^}]+)\}\s*from\s*[\"']([^\"']+)[\"']",
38
+ re.MULTILINE,
39
+ )
40
+ DEFAULT_IMPORT = re.compile(
41
+ r"\bimport\s+(?!type\b)([A-Za-z_$][\w$]*)\s+from\s*[\"']([^\"']+)[\"']",
42
+ re.MULTILINE,
43
+ )
44
+ DEFAULT_EXPORT_IDENTIFIER = re.compile(
45
+ r"\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?",
46
+ re.MULTILINE,
47
+ )
48
+ DEFAULT_EXPORT = re.compile(r"\bexport\s+default\s+", re.MULTILINE)
49
+ NAMED_REEXPORT = re.compile(
50
+ r"\bexport\s*\{([^}]+)\}\s*from\s*[\"']([^\"']+)[\"']",
51
+ re.MULTILINE,
52
+ )
53
+ STAR_REEXPORT = re.compile(
54
+ r"\bexport\s*\*\s*from\s*[\"']([^\"']+)[\"']",
55
+ re.MULTILINE,
56
+ )
57
+ FAKER = re.compile(r"\bfaker\s*\.", re.IGNORECASE)
58
+ PLACEHOLDER_VALUE = re.compile(
59
+ r"lorem ipsum|john doe|jane doe|dummy data|example@example"
60
+ r"|\b(?:mock|fake|dummy|placeholder)(?:Data|Users?|Orders?|Customers?|Rows?|Records?)\b",
61
+ re.IGNORECASE,
62
+ )
63
+ EXPLICIT_MOCK_TOKEN = {"mock", "fake", "dummy", "placeholder", "sample", "fixture"}
64
+ OPERATIONAL_TOKEN = {
65
+ "user", "users", "order", "orders", "customer", "customers", "account",
66
+ "accounts", "invoice", "invoices", "payment", "payments", "transaction",
67
+ "transactions", "record", "records", "row", "rows", "ticket", "tickets",
68
+ "message", "messages", "notification", "notifications", "activity",
69
+ "activities", "event", "events", "metric", "metrics", "analytics",
70
+ "dashboard", "task", "tasks", "note", "notes", "product", "products",
71
+ "member", "members", "project", "projects", "job", "jobs", "lead",
72
+ "leads", "contact", "contacts", "subscription", "subscriptions",
73
+ "deployment", "deployments", "build", "builds",
74
+ }
75
+ STATIC_CONTENT_TOKEN = {
76
+ "feature", "features", "benefit", "benefits", "plan", "plans", "pricing",
77
+ "testimonial", "testimonials", "story", "stories", "comparison",
78
+ "comparisons", "faq", "faqs", "navigation", "nav", "logo", "logos",
79
+ "step", "steps", "tier", "tiers", "quote", "quotes",
80
+ }
81
+ JSX_TAG = re.compile(r"<[A-Za-z][A-Za-z0-9_.:-]*\b")
82
+ DIRECT_COLLECTION_PROP = re.compile(
83
+ r"<(?:DataGrid|Table|List|Grid)\b[^>]*\b(?:rows|dataSource|items|data)\s*=\s*"
84
+ r"\{\s*([A-Za-z_$][\w$]*)[^{}]{0,500}\}",
85
+ re.IGNORECASE | re.DOTALL,
86
+ )
87
+ VUE_LOOP = re.compile(
88
+ r"\bv-for\s*=\s*[\"'][^\"']*\bin\s+([A-Za-z_$][\w$]*)[^\"']*[\"']",
89
+ re.IGNORECASE,
90
+ )
91
+ SVELTE_LOOP = re.compile(
92
+ r"\{#each\s+([A-Za-z_$][\w$]*)\s+as\b",
93
+ re.IGNORECASE,
94
+ )
95
+ DIRECT_INLINE_MAP = re.compile(
96
+ r"\[\s*\{(?P<body>.{0,4000}?)\}\s*(?:,\s*\{.{0,4000}?\}\s*)*\]"
97
+ r"\s*\.map\s*\(",
98
+ re.DOTALL,
99
+ )
100
+ OPERATIONAL_FIELD = re.compile(
101
+ r"\b(?:id|email|role|status|amount|total|customer|order|user|invoice|payment|transaction)\s*:",
102
+ re.IGNORECASE,
103
+ )
104
+ STRONG_OPERATIONAL_FIELD = re.compile(
105
+ r"\b(?:email|role|amount|total|customer|order|user|invoice|payment|transaction)\s*:",
106
+ re.IGNORECASE,
107
+ )
108
+ STATIC_CONTENT_FIELD = re.compile(
109
+ r"\b(?:feature|title|description|benefit|plan|price|quote|question|answer|logo|step|tier)\s*:",
110
+ re.IGNORECASE,
111
+ )
112
+ STATE_DECLARATION = re.compile(
113
+ r"\b(?:export\s+)?(?:const|let|var)\s*\[\s*([A-Za-z_$][\w$]*)[^\]]*\]"
114
+ r"\s*(?::[^=;]+)?=\s*",
115
+ re.MULTILINE,
116
+ )
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class Declaration:
121
+ file: str
122
+ name: str
123
+ expression: str
124
+ line: int
125
+
126
+
127
+ def _identifier_tokens(name: str) -> list[str]:
128
+ expanded = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
129
+ return [token.lower() for token in re.split(r"[^A-Za-z0-9]+", expanded) if token]
130
+
131
+
132
+ def _read_initializer(source: str, start: int) -> str:
133
+ """Read a JavaScript initializer through its top-level semicolon."""
134
+
135
+ depth = 0
136
+ quote = ""
137
+ escaped = False
138
+ index = start
139
+ limit = min(len(source), start + 50000)
140
+ while index < limit:
141
+ char = source[index]
142
+ next_char = source[index + 1] if index + 1 < limit else ""
143
+ if quote:
144
+ if escaped:
145
+ escaped = False
146
+ elif char == "\\":
147
+ escaped = True
148
+ elif char == quote:
149
+ quote = ""
150
+ index += 1
151
+ continue
152
+ if char in "\"'`":
153
+ quote = char
154
+ elif char == "/" and next_char == "/":
155
+ newline = source.find("\n", index + 2, limit)
156
+ index = limit if newline < 0 else newline
157
+ continue
158
+ elif char == "/" and next_char == "*":
159
+ close = source.find("*/", index + 2, limit)
160
+ index = limit if close < 0 else close + 2
161
+ continue
162
+ elif char in "([{":
163
+ depth += 1
164
+ elif char in ")]}" and depth > 0:
165
+ depth -= 1
166
+ elif char == ";" and depth == 0:
167
+ return source[start:index]
168
+ elif char == "\n" and depth == 0:
169
+ remainder = source[index + 1:limit]
170
+ if re.match(
171
+ r"\s*(?:(?:export\s+)?(?:const|let|var|function|class)\b|"
172
+ r"import\b|export\s+(?:default|\{|\*))",
173
+ remainder,
174
+ ):
175
+ return source[start:index]
176
+ index += 1
177
+ return source[start:limit]
178
+
179
+
180
+ def _declarations(relative: str, source: str) -> dict[str, Declaration]:
181
+ found: dict[str, Declaration] = {}
182
+ for match in DECLARATION.finditer(source):
183
+ name = match.group(1)
184
+ found[name] = Declaration(
185
+ file=relative,
186
+ name=name,
187
+ expression=_read_initializer(source, match.end()),
188
+ line=source.count("\n", 0, match.start()) + 1,
189
+ )
190
+ for match in STATE_DECLARATION.finditer(source):
191
+ name = match.group(1)
192
+ found[name] = Declaration(
193
+ file=relative,
194
+ name=name,
195
+ expression=_read_initializer(source, match.end()),
196
+ line=source.count("\n", 0, match.start()) + 1,
197
+ )
198
+ return found
199
+
200
+
201
+ def _mock_reason(declaration: Declaration) -> str:
202
+ tokens = _identifier_tokens(declaration.name)
203
+ expression = declaration.expression
204
+ static_content = any(token in STATIC_CONTENT_TOKEN for token in tokens)
205
+ if any(token in EXPLICIT_MOCK_TOKEN for token in tokens) and not static_content:
206
+ return "explicit_mock_collection"
207
+ if FAKER.search(expression):
208
+ return "faker_collection"
209
+ if PLACEHOLDER_VALUE.search(expression):
210
+ return "placeholder_collection"
211
+ inline_objects = re.match(
212
+ r"\s*(?:(?:useState|ref|reactive|readonly|\$state)"
213
+ r"(?:<[^>]{1,500}>)?\s*\(\s*)?\[\s*\{",
214
+ expression,
215
+ ) is not None
216
+ operational_fields = len(OPERATIONAL_FIELD.findall(expression)) >= 2
217
+ strong_operational = STRONG_OPERATIONAL_FIELD.search(expression) is not None
218
+ if inline_objects and not static_content and (
219
+ any(token in OPERATIONAL_TOKEN for token in tokens)
220
+ or (operational_fields and strong_operational)
221
+ ):
222
+ return "inline_operational_collection"
223
+ if (
224
+ re.match(r"\s*Array\.from\s*\(", expression)
225
+ and any(token in OPERATIONAL_TOKEN for token in tokens)
226
+ and not static_content
227
+ and re.search(r"=>\s*\(?\s*\{", expression)
228
+ ):
229
+ return "generated_operational_collection"
230
+ return ""
231
+
232
+
233
+ def _mask_non_code(source: str) -> str:
234
+ """Mask comments and strings while preserving positions and newlines."""
235
+
236
+ masked = list(source)
237
+ index = 0
238
+ while index < len(source):
239
+ char = source[index]
240
+ next_char = source[index + 1] if index + 1 < len(source) else ""
241
+ if char == "/" and next_char == "/":
242
+ end = source.find("\n", index + 2)
243
+ end = len(source) if end < 0 else end
244
+ for position in range(index, end):
245
+ masked[position] = " "
246
+ index = end
247
+ continue
248
+ if char == "/" and next_char == "*":
249
+ end = source.find("*/", index + 2)
250
+ end = len(source) if end < 0 else end + 2
251
+ for position in range(index, end):
252
+ if masked[position] != "\n":
253
+ masked[position] = " "
254
+ index = end
255
+ continue
256
+ if char in "\"'`":
257
+ quote = char
258
+ end = index + 1
259
+ escaped = False
260
+ while end < len(source):
261
+ current = source[end]
262
+ if escaped:
263
+ escaped = False
264
+ elif current == "\\":
265
+ escaped = True
266
+ elif current == quote:
267
+ end += 1
268
+ break
269
+ end += 1
270
+ for position in range(index, end):
271
+ if masked[position] != "\n":
272
+ masked[position] = " "
273
+ index = end
274
+ continue
275
+ index += 1
276
+ return "".join(masked)
277
+
278
+
279
+ def _balanced_parenthesized(source: str, opening: int) -> str:
280
+ if opening < 0 or opening >= len(source) or source[opening] != "(":
281
+ return ""
282
+ depth = 0
283
+ for index in range(opening, min(len(source), opening + 50000)):
284
+ if source[index] == "(":
285
+ depth += 1
286
+ elif source[index] == ")":
287
+ depth -= 1
288
+ if depth == 0:
289
+ return source[opening:index + 1]
290
+ return ""
291
+
292
+
293
+ def _named_callback_renders_jsx(source: str, call: str) -> bool:
294
+ callback = re.match(r"\(\s*([A-Za-z_$][\w$]*)\s*(?:,|\))", call)
295
+ if not callback:
296
+ return False
297
+ callback_name = callback.group(1)
298
+ for declaration in _declarations("", source).values():
299
+ if (
300
+ declaration.name == callback_name
301
+ and "=>" in declaration.expression
302
+ and JSX_TAG.search(declaration.expression)
303
+ ):
304
+ return True
305
+ name = re.escape(callback_name)
306
+ function = re.search(
307
+ rf"\bfunction\s+{name}\s*\([^)]*\)\s*\{{.{{0,5000}}?\breturn\s+(<[A-Za-z][A-Za-z0-9_.:-]*\b)",
308
+ source,
309
+ re.DOTALL,
310
+ )
311
+ return function is not None
312
+
313
+
314
+ def _skip_balanced_backward(source: str, index: int) -> int:
315
+ """Return the index before a balanced parenthesized expression."""
316
+
317
+ depth = 0
318
+ quote = ""
319
+ escaped = False
320
+ while index >= 0:
321
+ char = source[index]
322
+ if quote:
323
+ if escaped:
324
+ escaped = False
325
+ elif char == "\\":
326
+ escaped = True
327
+ elif char == quote:
328
+ quote = ""
329
+ index -= 1
330
+ continue
331
+ if char in "\"'`":
332
+ quote = char
333
+ elif char == ")":
334
+ depth += 1
335
+ elif char == "(":
336
+ depth -= 1
337
+ if depth == 0:
338
+ return index - 1
339
+ index -= 1
340
+ return -1
341
+
342
+
343
+ def _map_collection_root(source: str, map_dot: int) -> str:
344
+ """Trace `collection.filter(...).map` back to its root identifier."""
345
+
346
+ index = map_dot - 1
347
+ candidate = ""
348
+ while index >= 0:
349
+ while index >= 0 and source[index].isspace():
350
+ index -= 1
351
+ if index >= 0 and source[index] == ")":
352
+ index = _skip_balanced_backward(source, index)
353
+ while index >= 0 and source[index].isspace():
354
+ index -= 1
355
+ end = index + 1
356
+ while index >= 0 and (source[index].isalnum() or source[index] in "_$"):
357
+ index -= 1
358
+ if end == index + 1:
359
+ return candidate
360
+ candidate = source[index + 1:end]
361
+ while index >= 0 and source[index].isspace():
362
+ index -= 1
363
+ if index >= 0 and source[index] == "?":
364
+ index -= 1
365
+ if index >= 0 and source[index] == ".":
366
+ index -= 1
367
+ continue
368
+ return candidate
369
+ return candidate
370
+
371
+
372
+ def _rendered_symbols(source: str) -> set[str]:
373
+ rendered: set[str] = set()
374
+ structural = _mask_non_code(source)
375
+ for match in re.finditer(r"(?:\?\.|\.)\s*map\s*\(", structural):
376
+ call = _balanced_parenthesized(structural, match.end() - 1)
377
+ if JSX_TAG.search(call) or _named_callback_renders_jsx(structural, call):
378
+ root = _map_collection_root(structural, match.start())
379
+ if root:
380
+ rendered.add(root)
381
+ rendered.update(match.group(1) for match in DIRECT_COLLECTION_PROP.finditer(structural))
382
+ rendered.update(match.group(1) for match in VUE_LOOP.finditer(source))
383
+ rendered.update(match.group(1) for match in SVELTE_LOOP.finditer(source))
384
+ return rendered
385
+
386
+
387
+ def _named_imports(source: str) -> dict[str, tuple[str, str]]:
388
+ imports: dict[str, tuple[str, str]] = {}
389
+ for match in NAMED_IMPORT.finditer(source):
390
+ module = match.group(2)
391
+ for raw_binding in match.group(1).split(","):
392
+ binding = raw_binding.strip()
393
+ if not binding:
394
+ continue
395
+ parts = re.split(r"\s+as\s+", binding, maxsplit=1)
396
+ imported = parts[0].strip()
397
+ local = parts[-1].strip()
398
+ if re.fullmatch(r"[A-Za-z_$][\w$]*", imported) and re.fullmatch(
399
+ r"[A-Za-z_$][\w$]*", local
400
+ ):
401
+ imports[local] = (module, imported)
402
+ for match in DEFAULT_IMPORT.finditer(source):
403
+ imports[match.group(1)] = (match.group(2), "default")
404
+ return imports
405
+
406
+
407
+ def _jsonc_object(path: Path) -> dict[str, object]:
408
+ try:
409
+ source = path.read_text(encoding="utf-8", errors="replace")
410
+ except OSError:
411
+ return {}
412
+ source = re.sub(r"/\*.*?\*/", "", source, flags=re.DOTALL)
413
+ source = re.sub(r"(^|\s)//.*$", r"\1", source, flags=re.MULTILINE)
414
+ source = re.sub(r",\s*([}\]])", r"\1", source)
415
+ try:
416
+ value = json.loads(source)
417
+ except json.JSONDecodeError:
418
+ return {}
419
+ return value if isinstance(value, dict) else {}
420
+
421
+
422
+ def _module_bases(root: Path, importer: str, module: str) -> Iterable[Path]:
423
+ if module.startswith("."):
424
+ yield (root / importer).parent / module
425
+ return
426
+
427
+ matched = False
428
+ for config_name in ("tsconfig.json", "jsconfig.json"):
429
+ config = _jsonc_object(root / config_name)
430
+ compiler = config.get("compilerOptions")
431
+ if not isinstance(compiler, dict):
432
+ continue
433
+ base_url = compiler.get("baseUrl", ".")
434
+ base = root / (base_url if isinstance(base_url, str) else ".")
435
+ paths = compiler.get("paths")
436
+ if not isinstance(paths, dict):
437
+ continue
438
+ for pattern, replacements in paths.items():
439
+ if not isinstance(pattern, str) or not isinstance(replacements, list):
440
+ continue
441
+ prefix, marker, suffix = pattern.partition("*")
442
+ if marker:
443
+ if not module.startswith(prefix) or not module.endswith(suffix):
444
+ continue
445
+ wildcard = module[len(prefix):len(module) - len(suffix) if suffix else None]
446
+ elif module == pattern:
447
+ wildcard = ""
448
+ else:
449
+ continue
450
+ for replacement in replacements:
451
+ if isinstance(replacement, str):
452
+ matched = True
453
+ yield base / replacement.replace("*", wildcard)
454
+ if not matched and module.startswith(("@/", "~/")):
455
+ yield root / "src" / module[2:]
456
+
457
+
458
+ def _resolve_module(root: Path, importer: str, module: str) -> str:
459
+ candidates: list[Path] = []
460
+ for base in _module_bases(root, importer, module):
461
+ candidates.append(base)
462
+ candidates.extend(Path(str(base) + suffix) for suffix in RESOLUTION_EXTENSIONS)
463
+ candidates.extend(base / ("index" + suffix) for suffix in RESOLUTION_EXTENSIONS)
464
+ for candidate in candidates:
465
+ try:
466
+ resolved = candidate.resolve()
467
+ resolved.relative_to(root.resolve())
468
+ except (OSError, ValueError):
469
+ continue
470
+ if resolved.is_file() and resolved.suffix.lower() in SOURCE_EXTENSIONS:
471
+ return resolved.relative_to(root.resolve()).as_posix()
472
+ return ""
473
+
474
+
475
+ def _source_files(tree: Path) -> dict[str, str]:
476
+ sources: dict[str, str] = {}
477
+ for directory, names, files in os.walk(tree, topdown=True, followlinks=False):
478
+ directory_path = Path(directory)
479
+ relative_directory = directory_path.relative_to(tree).as_posix()
480
+ prefix = "" if relative_directory == "." else relative_directory + "/"
481
+ names[:] = sorted(
482
+ name
483
+ for name in names
484
+ if not SOURCE_INDEX_EXCLUDE.search(prefix + name + "/")
485
+ and not (directory_path / name).is_symlink()
486
+ )
487
+ for name in sorted(files):
488
+ path = directory_path / name
489
+ relative = prefix + name
490
+ if path.is_symlink() or path.suffix.lower() not in SOURCE_EXTENSIONS:
491
+ continue
492
+ if SOURCE_INDEX_EXCLUDE.search(relative):
493
+ continue
494
+ try:
495
+ sources[relative] = path.read_text(encoding="utf-8", errors="replace")
496
+ except OSError:
497
+ continue
498
+ return sources
499
+
500
+
501
+ def _exported_declaration(
502
+ tree: Path,
503
+ declarations: dict[str, dict[str, Declaration]],
504
+ sources: dict[str, str],
505
+ source_file: str,
506
+ imported_name: str,
507
+ local_name: str,
508
+ seen: set[tuple[str, str]] | None = None,
509
+ trace: set[str] | None = None,
510
+ ) -> Declaration | None:
511
+ seen = set() if seen is None else seen
512
+ trace = set() if trace is None else trace
513
+ key = (source_file, imported_name)
514
+ if not source_file or key in seen or len(seen) >= 12:
515
+ return None
516
+ seen.add(key)
517
+ trace.add(source_file)
518
+ if imported_name != "default":
519
+ direct = declarations.get(source_file, {}).get(imported_name)
520
+ if direct is not None:
521
+ return direct
522
+ source = sources.get(source_file, "")
523
+ if imported_name == "default":
524
+ identifier = DEFAULT_EXPORT_IDENTIFIER.search(source)
525
+ if identifier:
526
+ direct = declarations.get(source_file, {}).get(identifier.group(1))
527
+ if direct is not None:
528
+ return direct
529
+ exported = DEFAULT_EXPORT.search(source)
530
+ if exported:
531
+ expression = _read_initializer(source, exported.end())
532
+ return Declaration(
533
+ file=source_file,
534
+ name=local_name,
535
+ expression=expression,
536
+ line=source.count("\n", 0, exported.start()) + 1,
537
+ )
538
+
539
+ for match in NAMED_REEXPORT.finditer(source):
540
+ module = match.group(2)
541
+ for raw_binding in match.group(1).split(","):
542
+ parts = re.split(r"\s+as\s+", raw_binding.strip(), maxsplit=1)
543
+ original = parts[0].strip()
544
+ exported_name = parts[-1].strip()
545
+ if exported_name != imported_name:
546
+ continue
547
+ target = _resolve_module(tree, source_file, module)
548
+ branch_trace = set(trace)
549
+ resolved = _exported_declaration(
550
+ tree,
551
+ declarations,
552
+ sources,
553
+ target,
554
+ original,
555
+ local_name,
556
+ seen,
557
+ branch_trace,
558
+ )
559
+ if resolved is not None:
560
+ trace.update(branch_trace)
561
+ return resolved
562
+ if imported_name != "default":
563
+ for match in STAR_REEXPORT.finditer(source):
564
+ target = _resolve_module(tree, source_file, match.group(1))
565
+ branch_trace = set(trace)
566
+ resolved = _exported_declaration(
567
+ tree,
568
+ declarations,
569
+ sources,
570
+ target,
571
+ imported_name,
572
+ local_name,
573
+ seen,
574
+ branch_trace,
575
+ )
576
+ if resolved is not None:
577
+ trace.update(branch_trace)
578
+ return resolved
579
+ return None
580
+
581
+
582
+ def _symbol_declaration(
583
+ tree: Path,
584
+ declarations: dict[str, dict[str, Declaration]],
585
+ sources: dict[str, str],
586
+ source_file: str,
587
+ symbol: str,
588
+ trace: set[str] | None = None,
589
+ ) -> Declaration | None:
590
+ trace = set() if trace is None else trace
591
+ direct = declarations.get(source_file, {}).get(symbol)
592
+ if direct is not None:
593
+ trace.add(source_file)
594
+ return direct
595
+ imported = _named_imports(sources.get(source_file, "")).get(symbol)
596
+ if imported is None:
597
+ return None
598
+ module, imported_name = imported
599
+ target = _resolve_module(tree, source_file, module)
600
+ branch_trace = set(trace)
601
+ resolved = _exported_declaration(
602
+ tree,
603
+ declarations,
604
+ sources,
605
+ target,
606
+ imported_name,
607
+ symbol,
608
+ trace=branch_trace,
609
+ )
610
+ if resolved is not None:
611
+ trace.update(branch_trace)
612
+ return resolved
613
+
614
+
615
+ def _referenced_symbol(expression: str) -> str:
616
+ match = re.match(r"\s*([A-Za-z_$][\w$]*)\s*(?:\?\.)?\.", expression)
617
+ if match and match.group(1) not in {"Array", "Object", "Math", "JSON"}:
618
+ return match.group(1)
619
+ derived = re.match(
620
+ r"\s*(?:useMemo|computed)\s*\(\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)"
621
+ r"\s*=>\s*\(?\s*([A-Za-z_$][\w$]*)\s*(?:\?\.|\.|\[)",
622
+ expression,
623
+ )
624
+ return derived.group(1) if derived else ""
625
+
626
+
627
+ def _bound_mock_reason(
628
+ tree: Path,
629
+ declarations: dict[str, dict[str, Declaration]],
630
+ sources: dict[str, str],
631
+ declaration: Declaration,
632
+ seen: set[tuple[str, str]] | None = None,
633
+ trace: set[str] | None = None,
634
+ ) -> tuple[Declaration, str] | None:
635
+ seen = set() if seen is None else seen
636
+ trace = set() if trace is None else trace
637
+ key = (declaration.file, declaration.name)
638
+ if key in seen or len(seen) >= 12:
639
+ return None
640
+ seen.add(key)
641
+ trace.add(declaration.file)
642
+ reason = _mock_reason(declaration)
643
+ if reason:
644
+ return declaration, reason
645
+ referenced = _referenced_symbol(declaration.expression)
646
+ if not referenced:
647
+ return None
648
+ upstream = _symbol_declaration(
649
+ tree, declarations, sources, declaration.file, referenced, trace,
650
+ )
651
+ if upstream is None:
652
+ return None
653
+ return _bound_mock_reason(tree, declarations, sources, upstream, seen, trace)
654
+
655
+
656
+ def _changed_digest(files: list[str], tree: Path) -> str:
657
+ digest = hashlib.sha256()
658
+ root = tree.resolve()
659
+ for relative in sorted(set(files)):
660
+ digest.update(relative.encode("utf-8", errors="surrogateescape"))
661
+ digest.update(b"\0")
662
+ path = tree / relative
663
+ try:
664
+ resolved = path.resolve()
665
+ resolved.relative_to(root)
666
+ if path.is_symlink():
667
+ digest.update(b"symlink\0")
668
+ digest.update(os.readlink(path).encode("utf-8", errors="surrogateescape"))
669
+ elif path.is_file():
670
+ digest.update(b"file\0")
671
+ digest.update(hashlib.sha256(path.read_bytes()).digest())
672
+ else:
673
+ digest.update(b"missing\0")
674
+ except (OSError, ValueError):
675
+ digest.update(b"unreadable\0")
676
+ digest.update(b"\0")
677
+ return digest.hexdigest()
678
+
679
+
680
+ def scan(files: list[str], tree: Path) -> tuple[int, dict[str, object] | None]:
681
+ """Return changed sources scanned and the first collection-bound hit."""
682
+
683
+ changed = {
684
+ relative
685
+ for relative in files
686
+ if Path(relative).suffix.lower() in SOURCE_EXTENSIONS
687
+ and not SOURCE_INDEX_EXCLUDE.search(relative)
688
+ }
689
+ sources = _source_files(tree)
690
+ scanned = sum(1 for relative in changed if relative in sources)
691
+ declarations = {
692
+ relative: _declarations(relative, source)
693
+ for relative, source in sources.items()
694
+ }
695
+
696
+ for render_file, source in sources.items():
697
+ if RENDER_EXCLUDE.search(render_file):
698
+ continue
699
+ rendered = _rendered_symbols(source)
700
+ for local_name in sorted(rendered):
701
+ resolution_trace = {render_file}
702
+ declaration = _symbol_declaration(
703
+ tree,
704
+ declarations,
705
+ sources,
706
+ render_file,
707
+ local_name,
708
+ resolution_trace,
709
+ )
710
+ if declaration is None:
711
+ continue
712
+ bound = _bound_mock_reason(
713
+ tree,
714
+ declarations,
715
+ sources,
716
+ declaration,
717
+ trace=resolution_trace,
718
+ )
719
+ if bound is None:
720
+ continue
721
+ backing, reason = bound
722
+ if changed.isdisjoint(resolution_trace):
723
+ continue
724
+ return scanned, {
725
+ "file": backing.file,
726
+ "line": backing.line,
727
+ "snippet": backing.name[:80],
728
+ "reason": reason,
729
+ "render_file": render_file,
730
+ }
731
+
732
+ if render_file in changed:
733
+ structural = _mask_non_code(source)
734
+ for match in DIRECT_INLINE_MAP.finditer(structural):
735
+ body = source[match.start("body"):match.end("body")]
736
+ call = _balanced_parenthesized(structural, match.end() - 1)
737
+ operational_fields = len(OPERATIONAL_FIELD.findall(body)) >= 2
738
+ strong_operational = STRONG_OPERATIONAL_FIELD.search(body) is not None
739
+ static_content = STATIC_CONTENT_FIELD.search(body) is not None
740
+ if JSX_TAG.search(call) and (
741
+ (operational_fields and strong_operational and not static_content)
742
+ or FAKER.search(body)
743
+ or PLACEHOLDER_VALUE.search(body)
744
+ ):
745
+ return scanned, {
746
+ "file": render_file,
747
+ "line": source.count("\n", 0, match.start()) + 1,
748
+ "snippet": "inline array map",
749
+ "reason": "direct_inline_operational_collection",
750
+ "render_file": render_file,
751
+ }
752
+ return scanned, None
753
+
754
+
755
+ def main() -> int:
756
+ files = [line.strip() for line in os.environ.get("_NM_FILES", "").splitlines() if line.strip()]
757
+ tree = Path(os.environ.get("_NM_TREE", ".")).resolve()
758
+ output = os.environ.get("_NM_OUT", "")
759
+ scanned, hit = scan(files, tree)
760
+ receipt = {
761
+ "schema_version": 2,
762
+ "scanned": scanned,
763
+ "hit": hit is not None,
764
+ "file": hit["file"] if hit else "",
765
+ "line": hit["line"] if hit else 0,
766
+ "snippet": hit["snippet"] if hit else "",
767
+ "reason": hit["reason"] if hit else "",
768
+ "render_file": hit["render_file"] if hit else "",
769
+ "base_sha": os.environ.get("_NM_BASE", ""),
770
+ "source_digest": _changed_digest(files, tree),
771
+ }
772
+ if output:
773
+ try:
774
+ destination = Path(output)
775
+ destination.parent.mkdir(parents=True, exist_ok=True)
776
+ temporary = destination.with_name(destination.name + ".tmp")
777
+ temporary.write_text(json.dumps(receipt, indent=2), encoding="utf-8")
778
+ temporary.replace(destination)
779
+ except OSError as error:
780
+ print(f"receipt write failed: {error}", file=os.sys.stderr)
781
+ return 2
782
+
783
+ if hit:
784
+ print(f"FAIL:{hit['file']}:{hit['line']}:{hit['snippet']}:{hit['reason']}")
785
+ elif scanned == 0:
786
+ print("SKIP:0")
787
+ else:
788
+ print(f"PASS:{scanned}")
789
+ return 0
790
+
791
+
792
+ if __name__ == "__main__":
793
+ raise SystemExit(main())