agentlinkops 0.6.11 → 0.6.16

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 (311) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +2 -2
  3. package/.mcp.json +0 -0
  4. package/CHANGELOG.md +35 -0
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/PACK-ARTIFACT.json +16 -0
  8. package/PACKS.json +591 -0
  9. package/README.md +34 -48
  10. package/bin/agentlinkops.mjs +0 -0
  11. package/bin/linktrail.mjs +0 -0
  12. package/cli/adapters/csv.js +0 -0
  13. package/cli/adapters/sqlite-crm.js +0 -0
  14. package/cli/adapters/suppliers.js +1 -1
  15. package/cli/admission.js +0 -0
  16. package/cli/agent-setup.js +101 -17
  17. package/cli/agentlinkops.mjs +0 -0
  18. package/cli/args.js +0 -0
  19. package/cli/campaign-bridge.js +325 -0
  20. package/cli/campaign-outcomes.js +107 -0
  21. package/cli/catalog.js +47686 -16398
  22. package/cli/check.js +40 -10
  23. package/cli/chronology.js +39 -0
  24. package/cli/citation.js +0 -0
  25. package/cli/client.js +2 -1
  26. package/cli/cloud-error.js +0 -0
  27. package/cli/commands.js +17 -8
  28. package/cli/config.js +0 -0
  29. package/cli/connection.js +92 -18
  30. package/cli/contact-adoption.js +91 -0
  31. package/cli/context-mcp.js +0 -0
  32. package/cli/context.js +13 -2
  33. package/cli/delivery.js +0 -0
  34. package/cli/disavow.js +0 -0
  35. package/cli/doctor.js +15 -4
  36. package/cli/env.js +0 -0
  37. package/cli/fleet.js +30 -19
  38. package/cli/gsc-browser.js +0 -0
  39. package/cli/import.js +7 -1
  40. package/cli/index-observations.js +0 -0
  41. package/cli/index-submissions.js +0 -0
  42. package/cli/key-store.js +115 -0
  43. package/cli/ledger.js +1 -0
  44. package/cli/lifecycle.js +0 -0
  45. package/cli/linktrail.mjs +0 -0
  46. package/cli/local-result.js +41 -6
  47. package/cli/main.js +85 -20
  48. package/cli/mcp-client.js +2 -2
  49. package/cli/migration.js +308 -0
  50. package/cli/mirror.js +0 -0
  51. package/cli/mix.js +0 -0
  52. package/cli/native-lock.js +0 -0
  53. package/cli/oauth-credentials.js +0 -0
  54. package/cli/oauth-flow.js +0 -0
  55. package/cli/placement-projection.js +106 -0
  56. package/cli/platforms.js +0 -0
  57. package/cli/prompt.js +25 -0
  58. package/cli/public-http.js +168 -0
  59. package/cli/receipt-performance.js +0 -0
  60. package/cli/receipts.js +0 -0
  61. package/cli/report.js +51 -19
  62. package/cli/setup-plan.js +0 -0
  63. package/cli/skill.js +0 -0
  64. package/cli/state.js +76 -29
  65. package/cli/status.js +14 -18
  66. package/cli/sync-lock.js +0 -0
  67. package/cli/sync.js +23 -4
  68. package/package.json +7 -8
  69. package/references/citation-panels.md +2 -0
  70. package/references/cli.md +2 -2
  71. package/references/disavow.md +1 -1
  72. package/references/discovery-handoff.md +1 -1
  73. package/references/local-crm-draft-handoff.md +1 -1
  74. package/references/portable-ledger-handoff.md +11 -10
  75. package/references/recipes/catalog.json +263 -7
  76. package/references/recipes/command-notes.md +5 -3
  77. package/references/recipes/internal-link-improvement.md +25 -0
  78. package/references/recipes/placement-reconciliation.md +53 -1
  79. package/references/recipes/qualified-campaign-handoff.md +0 -0
  80. package/references/recipes/site-context-brief.md +0 -0
  81. package/references/recipes/sourced-linkable-asset.md +0 -0
  82. package/references/reports.md +0 -0
  83. package/scripts/agentlinkops.py +0 -0
  84. package/scripts/disavow.py +0 -0
  85. package/scripts/linktrail.py +0 -0
  86. package/scripts/migrations/001_crm.sql +0 -0
  87. package/scripts/migrations/002_cloud_events.sql +0 -0
  88. package/scripts/migrations/003_disavow.sql +0 -0
  89. package/scripts/migrations/004_placement_costs.sql +0 -0
  90. package/scripts/outreach-crm-draft.py +0 -0
  91. package/scripts/portable-handoff.py +17 -4
  92. package/scripts/report.py +0 -0
  93. package/shared/admission-contract.js +3 -2
  94. package/shared/admission-report-contract.js +3 -2
  95. package/shared/agent-clients.js +0 -0
  96. package/shared/analytics-history-contract.js +15 -0
  97. package/shared/campaign-outcome-contract.js +12 -0
  98. package/shared/campaign-prospect-contract.js +23 -0
  99. package/shared/citation-evidence-contract.js +0 -0
  100. package/shared/classification-contract.js +54 -0
  101. package/shared/contact-record-contract.js +27 -0
  102. package/shared/disavow-contract.js +37 -9
  103. package/shared/exec-grammar.js +0 -0
  104. package/shared/index-observation-contract.js +5 -4
  105. package/shared/internal-links-contract.js +143 -0
  106. package/shared/lifecycle-contract.js +0 -0
  107. package/shared/lifecycle-money.js +0 -0
  108. package/shared/link-category-contract.js +35 -0
  109. package/shared/link-category.js +138 -0
  110. package/shared/link-labels.js +26 -0
  111. package/shared/link-summary-contract.js +17 -0
  112. package/shared/monitoring-chronology-export.js +57 -0
  113. package/shared/monitoring-read-contract.js +41 -0
  114. package/shared/observation-event.js +1 -1
  115. package/shared/onboarding.js +20 -0
  116. package/shared/opportunity-record.js +0 -0
  117. package/shared/response-contract.js +41 -5
  118. package/shared/search-connection-contract.js +2 -1
  119. package/shared/table-page-contract.js +13 -0
  120. package/skills/agentlinkops-assets/SKILL-DEPS.json +290 -0
  121. package/skills/agentlinkops-assets/SKILL.md +3 -1
  122. package/skills/agentlinkops-assets/agents/openai.yaml +1 -1
  123. package/skills/agentlinkops-assets/references/portable/references/cli.md +220 -0
  124. package/skills/agentlinkops-assets/references/portable/references/disavow.md +73 -0
  125. package/skills/agentlinkops-assets/references/portable/references/reports.md +53 -0
  126. package/skills/agentlinkops-assets/references/portable/scripts/agentlinkops.py +676 -0
  127. package/skills/agentlinkops-assets/references/portable/scripts/disavow.py +207 -0
  128. package/skills/agentlinkops-assets/references/portable/scripts/migrations/001_crm.sql +45 -0
  129. package/skills/agentlinkops-assets/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  130. package/skills/agentlinkops-assets/references/portable/scripts/migrations/003_disavow.sql +20 -0
  131. package/skills/agentlinkops-assets/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  132. package/skills/agentlinkops-assets/references/portable/scripts/report.py +113 -0
  133. package/skills/agentlinkops-assets/references/portable/templates/campaigns.v1.json +65 -0
  134. package/skills/agentlinkops-campaigns/SKILL-DEPS.json +336 -0
  135. package/skills/agentlinkops-campaigns/SKILL.md +7 -5
  136. package/skills/agentlinkops-campaigns/agents/openai.yaml +1 -1
  137. package/skills/agentlinkops-campaigns/references/first-campaign.md +31 -0
  138. package/skills/agentlinkops-campaigns/references/mailbox-campaign.md +46 -0
  139. package/skills/agentlinkops-campaigns/references/portable/references/cli.md +220 -0
  140. package/skills/agentlinkops-campaigns/references/portable/references/disavow.md +73 -0
  141. package/skills/agentlinkops-campaigns/references/portable/references/reports.md +53 -0
  142. package/skills/agentlinkops-campaigns/references/portable/scripts/agentlinkops.py +676 -0
  143. package/skills/agentlinkops-campaigns/references/portable/scripts/disavow.py +207 -0
  144. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/001_crm.sql +45 -0
  145. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  146. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/003_disavow.sql +20 -0
  147. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  148. package/skills/agentlinkops-campaigns/references/portable/scripts/report.py +113 -0
  149. package/skills/agentlinkops-campaigns/references/portable/templates/campaigns.v1.json +65 -0
  150. package/skills/agentlinkops-connect/SKILL-DEPS.json +104 -0
  151. package/skills/agentlinkops-connect/SKILL.md +2 -0
  152. package/skills/agentlinkops-connect/agents/openai.yaml +1 -1
  153. package/skills/agentlinkops-connect/references/agentlinkops.md +164 -61
  154. package/skills/agentlinkops-connect/references/placement-workflows.md +19 -0
  155. package/skills/agentlinkops-crm/SKILL-DEPS.json +381 -0
  156. package/skills/agentlinkops-crm/SKILL.md +26 -9
  157. package/skills/agentlinkops-crm/agents/openai.yaml +1 -1
  158. package/skills/agentlinkops-crm/references/portable/references/cli.md +220 -0
  159. package/skills/agentlinkops-crm/references/portable/references/disavow.md +73 -0
  160. package/skills/agentlinkops-crm/references/portable/references/local-crm-draft-handoff.md +76 -0
  161. package/skills/agentlinkops-crm/references/portable/references/portable-ledger-handoff.md +51 -0
  162. package/skills/agentlinkops-crm/references/portable/references/reports.md +53 -0
  163. package/skills/agentlinkops-crm/references/portable/scripts/agentlinkops.py +676 -0
  164. package/skills/agentlinkops-crm/references/portable/scripts/disavow.py +207 -0
  165. package/skills/agentlinkops-crm/references/portable/scripts/migrations/001_crm.sql +45 -0
  166. package/skills/agentlinkops-crm/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  167. package/skills/agentlinkops-crm/references/portable/scripts/migrations/003_disavow.sql +20 -0
  168. package/skills/agentlinkops-crm/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  169. package/skills/agentlinkops-crm/references/portable/scripts/outreach-crm-draft.py +121 -0
  170. package/skills/agentlinkops-crm/references/portable/scripts/portable-handoff.py +197 -0
  171. package/skills/agentlinkops-crm/references/portable/scripts/report.py +113 -0
  172. package/skills/agentlinkops-crm/references/portable/templates/campaigns.v1.json +65 -0
  173. package/skills/agentlinkops-discovery/SKILL-DEPS.json +301 -0
  174. package/skills/agentlinkops-discovery/SKILL.md +4 -2
  175. package/skills/agentlinkops-discovery/references/portable/references/cli.md +220 -0
  176. package/skills/agentlinkops-discovery/references/portable/references/disavow.md +73 -0
  177. package/skills/agentlinkops-discovery/references/portable/references/discovery-handoff.md +32 -0
  178. package/skills/agentlinkops-discovery/references/portable/references/reports.md +53 -0
  179. package/skills/agentlinkops-discovery/references/portable/scripts/agentlinkops.py +676 -0
  180. package/skills/agentlinkops-discovery/references/portable/scripts/disavow.py +207 -0
  181. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/001_crm.sql +45 -0
  182. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  183. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/003_disavow.sql +20 -0
  184. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  185. package/skills/agentlinkops-discovery/references/portable/scripts/report.py +113 -0
  186. package/skills/agentlinkops-discovery/references/portable/templates/campaigns.v1.json +65 -0
  187. package/skills/agentlinkops-discovery/references/saved-backlink-workflows.md +23 -0
  188. package/src/citations/adapter.js +0 -0
  189. package/src/citations/alert.js +0 -0
  190. package/src/citations/browser/chatgpt-citations.js +0 -0
  191. package/src/citations/browser/engine.js +0 -0
  192. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
  193. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
  194. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
  195. package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
  196. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
  197. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
  198. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
  199. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
  200. package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
  201. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
  202. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
  203. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
  204. package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
  205. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
  206. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
  207. package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
  208. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
  209. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
  210. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
  211. package/src/citations/browser/gen/core/providers/index.js +0 -0
  212. package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
  213. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
  214. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
  215. package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
  216. package/src/citations/browser/gen/lib/browser/display.js +0 -0
  217. package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
  218. package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
  219. package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
  220. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
  221. package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
  222. package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
  223. package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
  224. package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
  225. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
  226. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
  227. package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
  228. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
  229. package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
  230. package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
  231. package/src/citations/browser/gen/utils/agent-constants.js +0 -0
  232. package/src/citations/browser/providers.js +0 -0
  233. package/src/citations/browser/proxy-admission.js +0 -0
  234. package/src/citations/browser/shims/errors.js +0 -0
  235. package/src/citations/browser/shims/types.js +0 -0
  236. package/src/citations/browser/shims/utils.js +0 -0
  237. package/src/citations/contract.js +0 -0
  238. package/src/citations/egress-receipts.js +0 -0
  239. package/src/citations/evidence-integrity.js +0 -0
  240. package/src/citations/inventory.js +0 -0
  241. package/src/citations/login.js +0 -0
  242. package/src/citations/match.js +0 -0
  243. package/src/citations/panel-builder.js +0 -0
  244. package/src/citations/report.js +0 -0
  245. package/src/citations/runner.js +0 -0
  246. package/src/citations/stats.js +0 -0
  247. package/src/citations/sync.js +0 -0
  248. package/src/citations/watch.js +0 -0
  249. package/src/competitors/contract.js +17 -1
  250. package/src/competitors/domain-mix.js +0 -0
  251. package/src/context/ga4.js +0 -0
  252. package/src/context/gsc.js +0 -0
  253. package/src/context/site-profile.js +34 -121
  254. package/src/context/util.js +0 -0
  255. package/src/context-tool-registry.js +0 -0
  256. package/src/corpus/acquire.js +68 -32
  257. package/src/corpus/extract.js +147 -0
  258. package/src/corpus/frontier.js +151 -0
  259. package/src/corpus/seeds.js +0 -0
  260. package/src/discovery/contract.js +8 -0
  261. package/src/discovery/import.js +13 -1
  262. package/src/evidence-reference.js +0 -0
  263. package/src/index-observation.js +0 -0
  264. package/src/locator.js +0 -0
  265. package/src/mentions.js +193 -0
  266. package/src/page-acquisition/acquire.js +50 -0
  267. package/src/page-acquisition/bytes.js +138 -0
  268. package/src/page-acquisition/capture.js +11 -0
  269. package/src/page-acquisition/contracts.js +67 -0
  270. package/src/page-acquisition/documents.js +166 -0
  271. package/src/page-acquisition/http.js +307 -0
  272. package/src/page-acquisition/policy.js +20 -0
  273. package/src/page-acquisition/recovery.js +164 -0
  274. package/src/page-acquisition/render-failure.js +82 -0
  275. package/src/page-extraction/html.js +359 -0
  276. package/src/page-extraction/index.js +30 -0
  277. package/src/page-extraction/metadata.js +16 -0
  278. package/src/response-shaping.js +4 -3
  279. package/src/tool-views.js +23 -12
  280. package/src/verifier/fetch.js +4 -339
  281. package/src/verifier/index.js +81 -23
  282. package/src/verifier/observation-kind.js +0 -0
  283. package/src/verifier/page-quality.js +14 -0
  284. package/src/verifier/parser.js +2 -241
  285. package/src/verifier/readiness.js +64 -13
  286. package/src/verifier/render-evidence.js +25 -2
  287. package/src/verifier/robots.js +0 -0
  288. package/src/verifier/target.js +201 -0
  289. package/src/verifier/transition.js +18 -3
  290. package/src/verifier/url.js +31 -0
  291. package/src/watch-crawlability.js +144 -0
  292. package/templates/SITE.template.md +0 -0
  293. package/templates/campaigns.v1.json +0 -0
  294. package/skills/content-defingerprinting/SKILL.md +0 -29
  295. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -102
  296. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -26
  297. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -52
  298. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -21
  299. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -96
  300. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -432
  301. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -86
  302. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -460
  303. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -651
  304. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -27
  305. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -212
  306. package/skills/internal-linking-optimizer/SKILL.md +0 -56
  307. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -76
  308. package/skills/pre-publish-review/SKILL.md +0 -179
  309. package/skills/source-cited-content-builder/SKILL.md +0 -56
  310. package/skills/source-cited-content-builder/references/source-prompt.md +0 -19
  311. package/skills/source-cited-humanizer/SKILL.md +0 -20
@@ -1,651 +0,0 @@
1
- """Layer A: invisible Unicode / homoglyph space detection and cleaning."""
2
-
3
- from __future__ import annotations
4
-
5
- import unicodedata
6
- from collections import Counter
7
- from dataclasses import dataclass, field
8
- from difflib import SequenceMatcher
9
-
10
- # Format / invisible controls commonly used for steganography or broken pastes.
11
- STRIP_CODEPOINTS: frozenset[int] = frozenset(
12
- {
13
- 0x00AD, # soft hyphen
14
- 0x034F, # combining grapheme joiner
15
- 0x061C, # Arabic letter mark
16
- 0x115F, # Hangul choseong filler
17
- 0x1160, # Hangul jungseong filler
18
- 0x17B4, # Khmer vowel inherent AQ
19
- 0x17B5, # Khmer vowel inherent AA
20
- 0x180B, # Mongolian free variation selector-1
21
- 0x180C,
22
- 0x180D,
23
- 0x180E, # Mongolian vowel separator
24
- 0x200B, # zero width space
25
- 0x200C, # zero width non-joiner
26
- 0x200D, # zero width joiner
27
- 0x200E, # LRM
28
- 0x200F, # RLM
29
- 0x202A, # LRE
30
- 0x202B, # RLE
31
- 0x202C, # PDF
32
- 0x202D, # LRO
33
- 0x202E, # RLO
34
- 0x2060, # word joiner
35
- 0x2061, # function application
36
- 0x2062, # invisible times
37
- 0x2063, # invisible separator
38
- 0x2064, # invisible plus
39
- 0x2066, # LRI
40
- 0x2067, # RLI
41
- 0x2068, # FSI
42
- 0x2069, # PDI
43
- 0x206A, # inhibit symmetric swapping
44
- 0x206B,
45
- 0x206C,
46
- 0x206D,
47
- 0x206E,
48
- 0x206F,
49
- 0xFEFF, # BOM / ZWNBSP
50
- 0xFE00, # variation selectors
51
- 0xFE01,
52
- 0xFE02,
53
- 0xFE03,
54
- 0xFE04,
55
- 0xFE05,
56
- 0xFE06,
57
- 0xFE07,
58
- 0xFE08,
59
- 0xFE09,
60
- 0xFE0A,
61
- 0xFE0B,
62
- 0xFE0C,
63
- 0xFE0D,
64
- 0xFE0E,
65
- 0xFE0F,
66
- 0xFFF9, # interlinear annotation
67
- 0xFFFA,
68
- 0xFFFB,
69
- }
70
- )
71
-
72
- # Spaces that look like (or substitute for) U+0020.
73
- SPACE_HOMOGLYPHS: dict[int, str] = {
74
- 0x00A0: " ", # no-break space
75
- 0x1680: " ", # Ogham space mark
76
- 0x2000: " ", # en quad
77
- 0x2001: " ", # em quad
78
- 0x2002: " ", # en space
79
- 0x2003: " ", # em space
80
- 0x2004: " ", # three-per-em space
81
- 0x2005: " ", # four-per-em space
82
- 0x2006: " ", # six-per-em space
83
- 0x2007: " ", # figure space
84
- 0x2008: " ", # punctuation space
85
- 0x2009: " ", # thin space
86
- 0x200A: " ", # hair space
87
- 0x202F: " ", # narrow no-break space
88
- 0x205F: " ", # medium mathematical space
89
- 0x3000: " ", # ideographic space
90
- }
91
-
92
- # Optional confusable Latin lookalikes (aggressive mode only).
93
- LATIN_CONFUSABLES: dict[int, str] = {
94
- 0x0410: "A", # Cyrillic
95
- 0x0412: "B",
96
- 0x0415: "E",
97
- 0x041A: "K",
98
- 0x041C: "M",
99
- 0x041D: "H",
100
- 0x041E: "O",
101
- 0x0420: "P",
102
- 0x0421: "C",
103
- 0x0422: "T",
104
- 0x0425: "X",
105
- 0x0430: "a",
106
- 0x0435: "e",
107
- 0x043E: "o",
108
- 0x0440: "p",
109
- 0x0441: "c",
110
- 0x0443: "y",
111
- 0x0445: "x",
112
- 0x0456: "i",
113
- 0xFF21: "A", # fullwidth
114
- 0xFF22: "B",
115
- 0xFF23: "C",
116
- 0xFF24: "D",
117
- 0xFF25: "E",
118
- 0xFF26: "F",
119
- 0xFF27: "G",
120
- 0xFF28: "H",
121
- 0xFF29: "I",
122
- 0xFF2A: "J",
123
- 0xFF2B: "K",
124
- 0xFF2C: "L",
125
- 0xFF2D: "M",
126
- 0xFF2E: "N",
127
- 0xFF2F: "O",
128
- 0xFF30: "P",
129
- 0xFF31: "Q",
130
- 0xFF32: "R",
131
- 0xFF33: "S",
132
- 0xFF34: "T",
133
- 0xFF35: "U",
134
- 0xFF36: "V",
135
- 0xFF37: "W",
136
- 0xFF38: "X",
137
- 0xFF39: "Y",
138
- 0xFF3A: "Z",
139
- 0xFF41: "a",
140
- 0xFF42: "b",
141
- 0xFF43: "c",
142
- 0xFF44: "d",
143
- 0xFF45: "e",
144
- 0xFF46: "f",
145
- 0xFF47: "g",
146
- 0xFF48: "h",
147
- 0xFF49: "i",
148
- 0xFF4A: "j",
149
- 0xFF4B: "k",
150
- 0xFF4C: "l",
151
- 0xFF4D: "m",
152
- 0xFF4E: "n",
153
- 0xFF4F: "o",
154
- 0xFF50: "p",
155
- 0xFF51: "q",
156
- 0xFF52: "r",
157
- 0xFF53: "s",
158
- 0xFF54: "t",
159
- 0xFF55: "u",
160
- 0xFF56: "v",
161
- 0xFF57: "w",
162
- 0xFF58: "x",
163
- 0xFF59: "y",
164
- 0xFF5A: "z",
165
- }
166
-
167
- # Variation selectors beyond FE0x (VS17-VS256 in Supplementary Special-purpose)
168
- _VS_SUPPLEMENT = range(0xE0100, 0xE01F0)
169
-
170
-
171
- # Bidi / directional format controls (subset of strip set, finer inspect labels)
172
- _BIDI_CPS: frozenset[int] = frozenset(
173
- {
174
- 0x061C,
175
- 0x200E,
176
- 0x200F,
177
- 0x202A,
178
- 0x202B,
179
- 0x202C,
180
- 0x202D,
181
- 0x202E,
182
- 0x2066,
183
- 0x2067,
184
- 0x2068,
185
- 0x2069,
186
- }
187
- )
188
-
189
- # Directional marks and isolates are legitimate in mixed RTL/LTR prose. Inspect
190
- # them, but preserve them during the default clean. Paired LRE/RLE embeddings
191
- # (see _valid_bidi_embedding_indices) are preserved too; overrides and
192
- # unpaired embeddings remain destructive by default because they can reorder
193
- # unrelated spans.
194
- _PRESERVABLE_BIDI_CPS: frozenset[int] = frozenset(
195
- {
196
- 0x061C,
197
- 0x200E,
198
- 0x200F,
199
- 0x2066,
200
- 0x2067,
201
- 0x2068,
202
- 0x2069,
203
- }
204
- )
205
-
206
- # Zero-width family (common edit-based carriers)
207
- _ZW_FAMILY: frozenset[int] = frozenset({0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF, 0x180E})
208
-
209
-
210
- def _is_private_use(cp: int) -> bool:
211
- """BMP and supplementary private-use planes (Co: no portable meaning)."""
212
- return 0xE000 <= cp <= 0xF8FF or 0xF0000 <= cp <= 0xFFFFD or 0x100000 <= cp <= 0x10FFFD
213
-
214
-
215
- def _is_strip_cp(cp: int) -> bool:
216
- if cp in STRIP_CODEPOINTS:
217
- return True
218
- if cp in _VS_SUPPLEMENT:
219
- return True
220
- # Tag characters used in some stego schemes (U+E0001-U+E007F)
221
- if 0xE0001 <= cp <= 0xE007F:
222
- return True
223
- return bool(_is_private_use(cp))
224
-
225
-
226
- def _strip_kind(cp: int) -> str:
227
- """Finer-grained inspect kind for strip-class codepoints."""
228
- if 0xE0001 <= cp <= 0xE007F:
229
- return "tag_chars"
230
- if cp in _VS_SUPPLEMENT or 0xFE00 <= cp <= 0xFE0F or 0x180B <= cp <= 0x180D:
231
- return "variation_selector"
232
- if cp in _BIDI_CPS:
233
- return "bidi"
234
- if cp in _ZW_FAMILY:
235
- return "zwj_family"
236
- if _is_private_use(cp):
237
- return "private_use"
238
- return "strip"
239
-
240
-
241
- # Emoji presentation glue: zero-width joiner and text/emoji variation
242
- # selectors. These are invisible carriers when free-floating, but after an
243
- # emoji base they are part of the visible sequence (⚖️, 👨‍👩‍👧, ❤️‍🔥) and
244
- # stripping them visibly alters the text.
245
- EMOJI_GLUE_CODEPOINTS: frozenset[int] = frozenset({0x200D, 0xFE0E, 0xFE0F})
246
-
247
-
248
- def _is_emoji_glue(cp: int) -> bool:
249
- return cp in EMOJI_GLUE_CODEPOINTS
250
-
251
-
252
- def _is_emoji_base(cp: int) -> bool:
253
- """Return True for characters that can start or continue an emoji sequence."""
254
- if 0x1F000 <= cp <= 0x1FAFF:
255
- return True
256
- if 0x2190 <= cp <= 0x25FF: # arrows, technical symbols, enclosed symbols
257
- return True
258
- if 0x2600 <= cp <= 0x27BF: # misc symbols / dingbats / arrows
259
- return True
260
- if 0x2B00 <= cp <= 0x2BFF: # misc symbols and arrows
261
- return True
262
- if cp in (0x00A9, 0x00AE, 0x2122, 0x3030, 0x303D, 0x3297, 0x3299):
263
- return True
264
- # keycap bases
265
- return cp in (0x0023, 0x002A) or 0x0030 <= cp <= 0x0039
266
-
267
-
268
- # ZWNJ/ZWJ are orthographic inside complex scripts (Persian می‌روم, Devanagari
269
- # क्‍ष); flag emoji are an emoji base followed by tag chars (🏴󠁧󠁢󠁳󠁣󠁴󠁿); and a
270
- # handful of Cf codepoints are normal Arabic/Syriac orthography, not carriers.
271
- # So are Mongolian free variation selectors (choose a glyph of the preceding
272
- # letter), Khmer inherent vowels (invisible but phonemic), and Hangul fillers
273
- # (hold a jamo slot in a partial syllable). Each is only meaningful directly
274
- # after a base from its own script; isolated instances are contraband.
275
- _SCRIPT_JOINERS: frozenset[int] = frozenset({0x200C, 0x200D})
276
- _TAG_RANGE = range(0xE0020, 0xE0080)
277
- _ORTHOGRAPHIC_CF: frozenset[int] = frozenset(
278
- {0x0600, 0x0601, 0x0602, 0x0603, 0x0604, 0x0605, 0x06DD, 0x070F, 0x08E2, 0x110BD, 0x110CD}
279
- )
280
- _MONGOLIAN_FVS: frozenset[int] = frozenset({0x180B, 0x180C, 0x180D})
281
- _KHMER_VOWELS: frozenset[int] = frozenset({0x17B4, 0x17B5})
282
- _HANGUL_FILLERS: frozenset[int] = frozenset({0x115F, 0x1160})
283
- _SCRIPT_GLUE: frozenset[int] = _MONGOLIAN_FVS | _KHMER_VOWELS | _HANGUL_FILLERS
284
-
285
-
286
- def _joining_script(cp: int) -> str | None:
287
- """Return a broad script group where ZWJ/ZWNJ can be orthographic."""
288
- for start, end, name in (
289
- (0x0600, 0x08FF, "arabic"),
290
- (0x0900, 0x0DFF, "indic"),
291
- (0x0F00, 0x109F, "south-asian"),
292
- (0x1780, 0x17FF, "khmer"),
293
- (0x1800, 0x18AF, "mongolian"),
294
- ):
295
- if start <= cp <= end and unicodedata.category(chr(cp))[0] in ("L", "M"):
296
- return name
297
- return None
298
-
299
-
300
- def _is_cjk_ideograph(cp: int) -> bool:
301
- return (
302
- 0x3400 <= cp <= 0x4DBF
303
- or 0x4E00 <= cp <= 0x9FFF
304
- or 0xF900 <= cp <= 0xFAFF
305
- or 0x20000 <= cp <= 0x323AF
306
- )
307
-
308
-
309
- def _is_mongolian_base(cp: int) -> bool:
310
- return 0x1800 <= cp <= 0x18AF
311
-
312
-
313
- def _is_variation_selector(cp: int) -> bool:
314
- return cp in _VS_SUPPLEMENT or 0xFE00 <= cp <= 0xFE0F or 0x180B <= cp <= 0x180D
315
-
316
-
317
- def _valid_flag_tag_indices(text: str) -> set[int]:
318
- """Indices in complete subdivision-flag tag sequences."""
319
- valid: set[int] = set()
320
- i = 0
321
- while i < len(text):
322
- if ord(text[i]) != 0x1F3F4: # waving black flag
323
- i += 1
324
- continue
325
- j = i + 1
326
- while j < len(text) and 0xE0020 <= ord(text[j]) <= 0xE007E:
327
- j += 1
328
- if j > i + 1 and j < len(text) and ord(text[j]) == 0xE007F:
329
- valid.update(range(i + 1, j + 1))
330
- i = j + 1
331
- else:
332
- i += 1
333
- return valid
334
-
335
-
336
- def _valid_bidi_embedding_indices(text: str) -> set[int]:
337
- """Indices belonging to complete LRE/RLE ... PDF pairs, excluding overrides."""
338
- valid: set[int] = set()
339
- stack: list[tuple[int, int]] = []
340
- for index, char in enumerate(text):
341
- cp = ord(char)
342
- if cp in (0x202A, 0x202B, 0x202D, 0x202E):
343
- stack.append((cp, index))
344
- elif cp == 0x202C:
345
- if not stack:
346
- continue
347
- opener, opener_index = stack.pop()
348
- if opener in (0x202A, 0x202B):
349
- valid.update((opener_index, index))
350
- return valid
351
-
352
-
353
- def _is_mongolian_letter(cp: int) -> bool:
354
- return 0x1800 <= cp <= 0x18AF and unicodedata.category(chr(cp))[0] == "L"
355
-
356
-
357
- def _is_khmer_letter(cp: int) -> bool:
358
- return 0x1780 <= cp <= 0x17FF and unicodedata.category(chr(cp))[0] == "L"
359
-
360
-
361
- def _is_hangul_jamo(cp: int) -> bool:
362
- return (
363
- 0x1100 <= cp <= 0x11FF
364
- or 0xA960 <= cp <= 0xA97C # Hangul Jamo Extended-A
365
- or 0xD7B0 <= cp <= 0xD7C6 # Hangul Jamo Extended-B
366
- )
367
-
368
-
369
- def _is_glue(cp: int) -> bool:
370
- """Load-bearing invisible char: emoji glue, script joiner, flag tag char,
371
- or same-script filler/selector (Mongolian FVS, Khmer vowel, Hangul filler)."""
372
- return (
373
- _is_emoji_glue(cp)
374
- or _is_variation_selector(cp)
375
- or cp in _SCRIPT_JOINERS
376
- or cp in _TAG_RANGE
377
- or cp in _SCRIPT_GLUE
378
- )
379
-
380
-
381
- def _decide(
382
- ch: str,
383
- prev_kept: str | None,
384
- prev_input: str | None,
385
- next_input: str | None,
386
- *,
387
- valid_flag_tag: bool,
388
- valid_bidi_embedding: bool,
389
- normalize_spaces: bool,
390
- treat_confusables: bool,
391
- strip_emoji_glue: bool,
392
- strip_bidi: bool,
393
- ) -> tuple[str, str, str | None]:
394
- """Classify one input char for both inspect and clean.
395
-
396
- Returns ``(action, out_char, kind)`` where action is ``keep``, ``strip``
397
- or ``replace``; out_char is the surviving character for keep/replace; and
398
- kind is the inspect classification (None when not suspicious).
399
- """
400
- cp = ord(ch)
401
- if valid_bidi_embedding and not strip_bidi:
402
- return ("keep", ch, None)
403
- if cp in _PRESERVABLE_BIDI_CPS and not strip_bidi:
404
- return ("keep", ch, None)
405
- if prev_input is not None and not strip_emoji_glue:
406
- prev_cp = ord(prev_input)
407
- if cp in _VS_SUPPLEMENT and _is_cjk_ideograph(prev_cp):
408
- return ("keep", ch, None)
409
- if 0x180B <= cp <= 0x180D and _is_mongolian_base(prev_cp):
410
- return ("keep", ch, None)
411
- if 0xFE00 <= cp <= 0xFE0D and _is_cjk_ideograph(prev_cp):
412
- return ("keep", ch, None)
413
- if _is_emoji_glue(cp) and not strip_emoji_glue:
414
- if cp in (0xFE0E, 0xFE0F) and prev_input is not None and _is_emoji_base(ord(prev_input)):
415
- return ("keep", ch, None)
416
- if (
417
- cp == 0x200D
418
- and prev_kept is not None
419
- and next_input is not None
420
- and _is_emoji_base(ord(prev_kept))
421
- and _is_emoji_base(ord(next_input))
422
- ):
423
- return ("keep", ch, None)
424
- if not strip_emoji_glue:
425
- if cp in _SCRIPT_JOINERS and prev_input is not None and next_input is not None:
426
- prev_script = _joining_script(ord(prev_input))
427
- next_script = _joining_script(ord(next_input))
428
- if prev_script is not None and prev_script == next_script:
429
- return ("keep", ch, None)
430
- if cp in _TAG_RANGE and valid_flag_tag:
431
- return ("keep", ch, None)
432
- if cp in _MONGOLIAN_FVS and prev_kept is not None and _is_mongolian_letter(ord(prev_kept)):
433
- return ("keep", ch, None)
434
- if cp in _KHMER_VOWELS and prev_kept is not None and _is_khmer_letter(ord(prev_kept)):
435
- return ("keep", ch, None)
436
- if cp in _HANGUL_FILLERS and prev_kept is not None and _is_hangul_jamo(ord(prev_kept)):
437
- return ("keep", ch, None)
438
- if cp in _ORTHOGRAPHIC_CF:
439
- return ("keep", ch, None)
440
- if _is_strip_cp(cp):
441
- return ("strip", "", _strip_kind(cp))
442
- if normalize_spaces and cp in SPACE_HOMOGLYPHS:
443
- return ("replace", SPACE_HOMOGLYPHS[cp], "space")
444
- if treat_confusables and cp in LATIN_CONFUSABLES:
445
- return ("replace", LATIN_CONFUSABLES[cp], "confusable")
446
- if unicodedata.category(ch) == "Cf" and cp not in SPACE_HOMOGLYPHS:
447
- return ("strip", "", "other_cf")
448
- return ("keep", ch, None)
449
-
450
-
451
- def _char_label(ch: str) -> str:
452
- cp = ord(ch)
453
- name = unicodedata.name(ch, "UNKNOWN")
454
- cat = unicodedata.category(ch)
455
- return f"U+{cp:04X} {name} ({cat})"
456
-
457
-
458
- def _hit_confidence(kind: str) -> str:
459
- """Layer A hits are edit-based carriers; space homoglyphs are weaker context."""
460
- return "informational" if kind == "space" else "probable"
461
-
462
-
463
- @dataclass
464
- class CharHit:
465
- codepoint: int
466
- char: str
467
- label: str
468
- count: int
469
- kind: str # strip | bidi | tag_chars | variation_selector | zwj_family | private_use | space | confusable | other_cf
470
- samples: list[int] = field(default_factory=list) # character offsets
471
-
472
-
473
- @dataclass
474
- class TextInspectReport:
475
- length: int
476
- suspicious_total: int
477
- hits: list[CharHit]
478
- notes: list[str] = field(default_factory=list)
479
-
480
- def to_dict(self) -> dict:
481
- return {
482
- "length": self.length,
483
- "suspicious_total": self.suspicious_total,
484
- "hits": [
485
- {
486
- "codepoint": f"U+{h.codepoint:04X}",
487
- "label": h.label,
488
- "count": h.count,
489
- "kind": h.kind,
490
- "confidence": _hit_confidence(h.kind),
491
- "sample_offsets": h.samples[:10],
492
- }
493
- for h in self.hits
494
- ],
495
- "notes": self.notes,
496
- }
497
-
498
-
499
- def inspect_text(
500
- text: str,
501
- *,
502
- aggressive: bool = False,
503
- strip_emoji_glue: bool = False,
504
- ) -> TextInspectReport:
505
- buckets: dict[tuple[int, str], list[int]] = {}
506
- prev_kept: str | None = None
507
- valid_flag_tags = _valid_flag_tag_indices(text)
508
- valid_bidi_embeddings = _valid_bidi_embedding_indices(text)
509
- for i, ch in enumerate(text):
510
- action, out_char, kind = _decide(
511
- ch,
512
- prev_kept,
513
- text[i - 1] if i > 0 else None,
514
- text[i + 1] if i + 1 < len(text) else None,
515
- valid_flag_tag=i in valid_flag_tags,
516
- valid_bidi_embedding=i in valid_bidi_embeddings,
517
- normalize_spaces=True,
518
- treat_confusables=aggressive,
519
- strip_emoji_glue=strip_emoji_glue,
520
- strip_bidi=True,
521
- )
522
- if kind is None:
523
- # Kept; glue (emoji/script joiner/tag) does not advance the
524
- # "previous kept" base so ZWJ chains and flag runs stay bound.
525
- if not _is_glue(ord(ch)):
526
- prev_kept = out_char
527
- continue
528
- key = (ord(ch), kind)
529
- buckets.setdefault(key, []).append(i)
530
- if action == "replace":
531
- prev_kept = out_char
532
- # strip: prev_kept unchanged
533
-
534
- hits: list[CharHit] = []
535
- total = 0
536
- for (cp, kind), offsets in sorted(buckets.items(), key=lambda x: (-len(x[1]), x[0][0])):
537
- ch = chr(cp)
538
- hits.append(
539
- CharHit(
540
- codepoint=cp,
541
- char=ch,
542
- label=_char_label(ch),
543
- count=len(offsets),
544
- kind=kind,
545
- samples=offsets[:10],
546
- )
547
- )
548
- total += len(offsets)
549
-
550
- notes = [
551
- "Layer A only: invisible/format Unicode and space homoglyphs (edit-based carriers).",
552
- "Statistical (token-sampling) watermarks are not detectable here; use Layer B rewrite.",
553
- "Inspect kinds: strip, bidi, tag_chars, variation_selector, zwj_family, private_use, space, confusable, other_cf.",
554
- "Load-bearing invisibles are preserved by default during cleaning: emoji glue, CJK/Mongolian variation selectors, script joiners, complete flag tag sequences, same-script fillers/selectors (Mongolian FVS, Khmer inherent vowels, Hangul jamo fillers), RTL directional marks/paired embeddings, and orthographic Arabic/Syriac Cf marks. Inspection still reports bidi controls. Use explicit strip flags only after review.",
555
- ]
556
- if not hits:
557
- notes.append(
558
- "No deterministic Layer A (invisible Unicode/format) carriers detected; "
559
- "statistical and pixel-domain marks are out of scope here."
560
- )
561
- return TextInspectReport(length=len(text), suspicious_total=total, hits=hits, notes=notes)
562
-
563
-
564
- def clean_text(
565
- text: str,
566
- *,
567
- nfkc: bool = False,
568
- aggressive_homoglyphs: bool = False,
569
- normalize_spaces: bool = True,
570
- strip_emoji_glue: bool = False,
571
- strip_bidi: bool = False,
572
- ) -> tuple[str, dict]:
573
- """Return cleaned text and a stats dict."""
574
- removed: Counter[str] = Counter()
575
- replaced: Counter[str] = Counter()
576
- out_chars: list[str] = []
577
- prev_kept: str | None = None
578
- valid_flag_tags = _valid_flag_tag_indices(text)
579
- valid_bidi_embeddings = _valid_bidi_embedding_indices(text)
580
-
581
- for i, ch in enumerate(text):
582
- action, out_char, _kind = _decide(
583
- ch,
584
- prev_kept,
585
- text[i - 1] if i > 0 else None,
586
- text[i + 1] if i + 1 < len(text) else None,
587
- valid_flag_tag=i in valid_flag_tags,
588
- valid_bidi_embedding=i in valid_bidi_embeddings,
589
- normalize_spaces=normalize_spaces,
590
- treat_confusables=aggressive_homoglyphs,
591
- strip_emoji_glue=strip_emoji_glue,
592
- strip_bidi=strip_bidi,
593
- )
594
- if action == "keep":
595
- out_chars.append(out_char)
596
- # Glue (emoji/script joiner/tag) does not advance the "previous
597
- # kept" base, so ZWJ chains (❤️‍🔥) and flag runs stay bound.
598
- if not _is_glue(ord(ch)):
599
- prev_kept = out_char
600
- elif action == "replace":
601
- out_chars.append(out_char)
602
- replaced[_char_label(ch)] += 1
603
- prev_kept = out_char
604
- else: # strip
605
- removed[_char_label(ch)] += 1
606
- # prev_kept unchanged
607
-
608
- result = "".join(out_chars)
609
- nfkc_changed = False
610
- if nfkc:
611
- before = result
612
- result = unicodedata.normalize("NFKC", result)
613
- if result != before:
614
- nfkc_changed = True
615
- changed_inputs = sum(
616
- end - start
617
- for operation, start, end, _new_start, _new_end in SequenceMatcher(
618
- None, before, result, autojunk=False
619
- ).get_opcodes()
620
- if operation != "equal"
621
- )
622
- replaced["NFKC_normalize"] += changed_inputs or 1
623
-
624
- # Collapse runs of spaces only if we introduced space replacements? Keep conservative: no.
625
-
626
- stats = {
627
- "input_length": len(text),
628
- "output_length": len(result),
629
- "removed": dict(removed),
630
- "replaced": dict(replaced),
631
- "removed_count": sum(removed.values()),
632
- "replaced_count": sum(replaced.values()),
633
- "nfkc_changed": nfkc_changed,
634
- }
635
- return result, stats
636
-
637
-
638
- def human_report(report: TextInspectReport) -> str:
639
- lines = [
640
- f"Length: {report.length} chars",
641
- f"Suspicious: {report.suspicious_total}",
642
- ]
643
- if report.hits:
644
- lines.append("Hits:")
645
- for h in report.hits:
646
- lines.append(
647
- f" [{h.kind}/{_hit_confidence(h.kind)}] {h.label} x{h.count} @ {h.samples[:5]}"
648
- )
649
- for n in report.notes:
650
- lines.append(f"Note: {n}")
651
- return "\n".join(lines)
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Prove this installed copy of the engine is the pinned upstream one.
3
-
4
- A vendored dependency nobody checks is a dependency that has quietly drifted.
5
- Exit 0 = matches UPSTREAM.json, 1 = drifted or missing.
6
- """
7
- import hashlib, io, json, os, sys
8
-
9
- here = os.path.dirname(os.path.abspath(__file__))
10
- manifest = json.load(io.open(os.path.join(here, "UPSTREAM.json"), encoding="utf-8"))
11
- bad = []
12
- for name, expected in manifest["sha256"].items():
13
- path = os.path.join(here, "vendor", name)
14
- if not os.path.exists(path):
15
- bad.append((name, "MISSING"))
16
- continue
17
- actual = hashlib.sha256(io.open(path, "rb").read()).hexdigest()
18
- if actual != expected:
19
- bad.append((name, "MODIFIED"))
20
- if bad:
21
- print("wm-verify: vendored engine has DRIFTED from %s" % manifest["pinned_commit"][:12])
22
- for name, why in bad:
23
- print(" %-24s %s" % (name, why))
24
- print("\nRestore with ./revendor.sh, and never edit vendor/ in place.")
25
- sys.exit(1)
26
- print("wm-verify: %d files match upstream %s (%s)"
27
- % (len(manifest["sha256"]), manifest["pinned_commit"][:12], manifest["pinned_commit_date"][:10]))