Hi Sam, some comments about the lesson backup:
1) First of all, the encode_content_links() function. I must confess I never had seen that. Do we really need that complexity. All the rest of modules look really simpler. Do we need to have encoded versions of links like "edit one page" or so? I cannot imagine where one teacher could be interested into using that URL. In any case, if you consider it works and have the restore code under control, no problem here but one observation:
As far as that static method is executed for every content being included in backup. I'd suggest to cache (static vars) both $pattern and $replacement mainly to avoid them being calculated zillions of times.
2) About the structure, I think it's well defined, both the elements (always using the intermediate "plural" as extra level) and their parent-child relationship. One little detail is that I use to avoid duplicates in names like, for example, one final element named "answer" under the nested element "answer". No sure if it could be renamed, for example, to "answer_text" if it's one text or so. If you need to change the name of one element from its DB name to an alternative one you can use the $element->set_source_alias('dbname', 'xmlname') method.
3) About the sources. I think you are "using too many condiitons". For example, the $attempt source. There, you are adding conditions to "grand-grand-parent" $page->id and with "grand-grand-grand-parent" $lesson->id. And that's unnecessary completely! If one $attempt only belongs to only one $answer, it's 100% enough to use only the first condition (PARENT_ID) as far and that's the valid FK to fetch all the attempts for the given answer. The extra conditions won't hurt, but won't add anything from a relational pov. Note that this os only true if the condition "belongs to only one parent" is true. In the model you can find elements that need more than the PARENT_ID condition, but as far as Moodle is so-so id-oriented, I think 99% of sources can be expressed in that way (simple condition).
Also note, as a side effect of this, that, when the condition "belongs to only one parent" is true, there is no need to include those FK fields at all in the child element, so the "answerid" in attempt isn't necessary at all, for example, that should give you a hint that only one condition is necessary. It seems that you have followed that rule ok and your structure is clean of child FK fields.
4) About ID annotations, I've seen a bunch of them related to users, cool. One quick question is... if you've declared the "answers" table as being part of the "userinfo" being exluded... where is the corresponding "user" annotation in that table? Seems that something is wrong as far as each "userinfo" table should be providing some "user annotation". Note I haven't verified it, jut surprised me. Also about annotations... I guess you've looked for all them but only have found "user" ones, correct? No scales/outcomes/groups/groupings... correct?
5) About file annotations... I really don't know if those are all the ones allowed by the lesson activity. In theory, any field able to be used by the html-editor uses to be a candidate to have its own file area. Only those two are the ones along all the tables (usually text fields) ? For example answers and response cannot have their own images and so on?
Apart of that, I really think you have missed one file_area, a pretty obvious one (common in all the modules). I won't give you more clues... lol.
It's looking great! sure in next iteration it's 100% perfect, thanks!