# Reading the other ANU sources directly **This is a fallback. If you can run `anu-pandc`, stop reading and run it.** Its companion, [reading-pandc-directly.md](reading-pandc-directly.md), covers Programs & Courses. This one covers the sources the CLI reads alongside it: the **Policy Library**, **University legislation**, the **class timetable**, the **exam timetable** and the **university calendar**. Same caveat as the companion — hand-fetching re-derives parsers that already exist, and the CLI is the right answer wherever it runs: ```bash uvx anu-pandc policy get ANUP_004603 ``` Everything below was checked against the live sites on 2026-09-20 (the exam timetable on 2026-09-28). ## What lives where A question about coursework usually needs more than one of these, and they answer different kinds of question. Getting the source wrong is the commonest way to give a confidently wrong answer. | Question | Source | |----------|--------| | What must happen? Who may decide it? | **Legislation** — Statutes, Rules, Orders. Binding, federal law. | | How does the University apply that? | **Policy Library** — policy, then procedure under it. | | What does this course require of a student? | **P&C** course page, then the **class summary** for the real assessment. | | When does it happen, university-wide? | **University calendar** — census, breaks, exam periods, results. | | When and where does a class meet? | **MyTimetable** — scheduled, not delivered. | | When and where is a course's exam? | **Exam timetable** — only while its exam event is open. | Legislation beats policy, policy beats procedure, and a class summary beats all of them for what a particular class actually did. If a policy and a Rule disagree, the Rule wins and the policy is out of date. ## 1. The ANU Policy Library Base: `https://policies.anu.edu.au`. Server-rendered HTML; no JavaScript needed, no year in any URL — a document has an effective date, and only the current version is published. Documents are numbered `ANUP_004603`. | What | URL | |------|-----| | One document | `/ppl/document/ANUP_004603` | | Every document, A–Z | `/ppl/title/index.htm` (≈495 rows, one page) | | Everything of one type | `/ppl/view_all/index.htm?subdoctype_id=Policy` | | Everything under a topic | `/ppl/view_all/index.htm?ssTopic=Students&ssSubTopic=Assessment` | | Keyword search | `/ppl/search_results/index.htm?searchtype=QUICK&ssUserFullText=delegated+authority` | `subdoctype_id` is one of `Policy`, `Procedure`, `Standard`, `Guideline`, `Form`. A document page has three parts worth reading: - `div#convertedcontent` — the document body, converted from Word. - `div#info_block` — a two-column table of the governance metadata: document type and number, **effective date**, **next review date**, **responsible officer**, **approved by**, **contact area**, and **Authority**, which links the legislation the document is made under. This is what tells you whether a document still binds anyone. - a Related Content box linking the policy to its procedures and forms. ### Gotchas - **Clause numbers are in the markup, not the text.** A policy cites its own clauses ("in accordance with Clause 73"), and a single run of clauses is split across dozens of `
    ` elements, each carrying `start="13"`. Read `start`, or every number you quote will be wrong. - Lettered sub-clauses are `
      ` and roman ones `
        `. - The page leaks its templating language into HTML comments (`convStringANU = wcmDynamicConversion(...)`). Drop comments before taking text, or they land in the middle of the document. - **Search shows only the first five hits per document type**, with the true count in the group heading ("Policies (5 of 9)"). Use `view_all` for the rest. The "View all" link the page offers has the wrong `subdoctype_id` on it; build the URL yourself. - The "Contact Area" column is sometimes an internal officer id (`260820251156`) rather than a name. That is the site's own quirk. - A "Printable version (PDF)" link is `/ppl/pdfdownload/`, where `` is unrelated to the document number. ## 2. University legislation ANU's Statutes, Rules and Orders are made by Council or the Vice-Chancellor under s 50 of the *Australian National University Act 1991*. They have the status of federal law and are registered on the Federal Register of Legislation. That means two sources, and you need both. **What applies to ANU** — `https://www.anu.edu.au/about/governance/legislation`. Server-rendered, grouped under `h3` into Acts, Statutes, Rules and Orders, each item a `ul.linklist` entry linking to an ANU page. The Register has no way to ask "everything ANU made", so this list is the answer to which titles count. The ANU index page carries no Register ids; an item's own page does, in the deep links to the document text. **Status and text** — the Register. - Metadata, OData: `https://api.prod.legislation.gov.au/v1/titles?$filter=...` ``` $filter=contains(name,'Coursework Awards Rule') and isInForce eq true $select=id,name,collection,subCollection,status,isInForce,isPrincipal,makingDate $expand=authorisedBy,versions ``` - Human page, and the one to cite: `https://www.legislation.gov.au/F2024L01752/latest/text` - **Document text**, which is what you actually want: ``` https://www.legislation.gov.au/{id}/latest/{version-start-date}/text/original/epub/OEBPS/document_1/document_1.html ``` where `{version-start-date}` is `YYYY-MM-DD`, taken from the `isLatest` entry in `versions`. ### Gotchas - **`/latest/text` without a date is a JavaScript shell.** It returns ~76 KB of Angular and no document. The date-bearing EPUB URL above is the text. So a fetch is always two requests: OData for the version date, then the text. - **The text is served with no charset.** `requests` then guesses ISO-8859-1 and every em dash becomes `—`. Decode as UTF-8, or hand the bytes to a parser that reads the document's own declaration. - `$orderby` is rejected by the API (HTTP 400). Sort client-side. - So is any `$filter` over `authorisedBy` — the server throws a LINQ error. You can read `authorisedBy` with `$expand`, you just cannot filter on it. Which is why enumerating ANU's instruments goes through the ANU index page. - **A name matches every version ever made.** "Coursework Awards Rule" returns sixteen titles, fifteen of them repealed. Filter on `isInForce` and `isPrincipal`, and check `status` before quoting anything. - ANU's education Rules do *not* have "Australian National University" in their names — *Coursework Awards Rule 2024*, *Academic Integrity Rule 2021*, *Assessment Rule 2016*. Searching the Register for the University's name finds the Statutes and misses the Rules. - Ids: `C####A#####` an Act, `F####L#####` a legislative instrument, `F####N#####` notifiable, `C####G#####` a gazette notice. ## 3. The class timetable (Allocate+ Web Publisher) `https://mytimetable.anu.edu.au` is a JavaScript application over a small REST backend, so fetching the page gets you an empty shell. Query the backend. There are two instances, one per parity of the year: `/even/` currently holds 2026 and `/odd/` 2025. There is no year parameter — **the instance is the year** — so any other year cannot be read here at all. ```bash curl -s -X POST https://mytimetable.anu.edu.au/even/rest/timetable/subjects \ --data 'search-term=COMP3300&days=1&days=2&days=3&days=4&days=5&days=6&days=0&start-time=00:00&end-time=23:00' ``` The response is a JSON object keyed by offering (`COMP3300_S2_1_8682`, whose last segment is the P&C class number). Each offering has `manager` (a person — the only name in this data), `children` (co-taught offerings), and `activities`, a dict of activity records with `activity_group_code` (LecA, ComA…), `activity_code`, `activity_type`, `day_of_week`, `start_time`, `duration` in minutes, `location`, `description`, `semester_description`, `week_pattern`, and `activitiesDays` — the explicit list of dates as `d/m/yyyy`. Other endpoints under `../rest/timetable/`: `locations` and `studentsets` (POST searches), `subject//sections`, `subject//activity_groups`, `locations///`. ### Gotchas - **`activitiesDays` is the only thing that locates an activity in time.** A row's day and time are a weekly *pattern*; the dates say which weeks it actually runs. A search returns every teaching period of the year at once, so a response with both semesters in it is normal and the period is not implied by the date you are asking about. To answer "what is on in the week of 21 September 2026", filter `activitiesDays` to Mon–Sun around that date and report only what survives — do not read a weekly pattern and assume it applies. Deciding between First and Second Semester by guessing where in the year a date falls is how this goes wrong; the dates are right there. - **Count sessions from `activitiesDays`**, not by decoding `week_pattern`. - **Drop `activity_type: "Clone"`.** Those duplicate a real slot, carry no location, and double every count made over them. - **Several streams of one group are alternatives.** ComA/01–04 are four offerings of the same lab; a student attends one. Counting all four turns 5 contact hours into 20. - `staff` is almost always `-`. The timetable does not say who teaches. The convener is on the class summary in P&C. - Co-taught codes are in `description` (`COMP3300_S2_(01)-ComA/01 + COMP6330_S2_(01)-ComA/01`). A code is followed by an underscore, which is a word character, so `\bCOMP\d{4}\b` never matches. Use a lookahead. - This is the **scheduled** timetable. It changes, and it is not evidence that anything was delivered. Required contact hours come from P&C and the class summaries; check the dates against the calendar's teaching breaks and public holidays. ## 4. The exam timetable `https://exams.anu.edu.au/timetable/login.php` — server-rendered PHP, no JavaScript needed. The Examinations Office publishes each exam *event* ("Semester 2 - End of Semester, 2026", "Semester 2 - In-Class & Online, 2026") as its own database, chosen with `db`. | What | How | |------|-----| | Events open right now | `GET login.php?db=0`: a link per event, `login.php?db=N` | | One event's search form | `GET login.php?db=N` | | Search | `POST login.php?db=N` with form field `Code=COMP1100,COMP2300` | | Next page of results | `GET login.php?skip=20`, then `skip=40`, … on the same session | ```bash curl -s -c jar -b jar 'https://exams.anu.edu.au/timetable/login.php?db=13' > /dev/null curl -s -c jar -b jar --data 'Code=COMP1100' 'https://exams.anu.edu.au/timetable/login.php?db=13' ``` Results are `table#table-compact`, one row per exam *per room*, with a header row naming the columns. The end-of-semester event has Exam Code, Exam Title, Assessment Type, Date (`Friday 20/11/2026`), Time (`2:00pm`), Writing Time and Reading Time (minutes), Venue, Building (the ANU building number) and Room. "Sorry, no exams were found." means just that. ### Gotchas - **Most of the year there is nothing here.** An event appears only when the Examinations Office releases it and closes on a fixed date; after that its page reads "Sorry, this feature is not available after DD/MM/YYYY". An empty or irrelevant index is a normal answer — say the exam timetable is not published yet, and give the calendar's exam period instead. Do not infer an exam date from anything else. - **`db` numbers are recycled slots, not ids.** `db=14` held Semester 1 2024, `db=1` Semester 1 2025, `db=13` Semester 2 2026. Always start from `db=0`; never guess or bookmark a number. - **Columns differ between events.** The in-class event has no Reading Time or Room. Read the header row; never index cells by position. - **The session carries state.** The pager links (`?skip=20`) name neither the event nor the search — the PHP session cookie does. And once a session has searched one event, a POST straight to another answers HTTP 500 until that event's page has been loaded with a GET. Load the event page, then search. - **One exam, many rows.** A large exam is published once per room (COMP1730: seven rooms). Group by exam code, date and time before counting exams. A student sits in one room, and the table does not say which. - **Co-taught courses share an exam code**: `COMP1110/COMP1140/COMP6710_Semester 2`. What follows the first underscore is the sitting, and may carry stray numbers (`COMP3300_Semester 2 / 3300`) or a second sitting (`MATH1014_Semester 2-1`). - **Search is a prefix match on each part of the code**, case-insensitive: `COMP6710` finds the combined code above, `COMP` finds every COMP exam, and `6710` finds nothing. - The form carries a hidden `_token`. The server accepts a search without it today; send it back if you have loaded the form anyway. - The page status beside the event name ("Final Timetable") is the release state. Quote it along with the date. ## 5. The university calendar Published twice, as a page and as a feed: | Purpose | URL | |---------|-----| | The page, and the thing to cite | `https://www.anu.edu.au/directories/university-calendar?year=YYYY` | | Machine-readable | `https://www.anu.edu.au/directories/university-calendar/YYYY/calendar.ics` | The feed works for 2026 (55 events) and 2027 (62), and is served with `cache-control: max-age=900`, so it is near-live. Each `VEVENT` has `DTSTART`, `SUMMARY` and `URL`. ### Gotchas - **Every event is a single day.** There is no `DTEND`; a range is published as two events, "… begins" and "… ends". Pair them yourself. - **"Return from X" is the first day *back*, not the last day off.** The two closing shapes do not mean the same thing: "Semester 1 ends" is inclusive — 29 May is the last day of semester — but "Return from teaching break" on 21 September 2026 is the Monday teaching *resumes*. Ending the break on it puts a teaching break over a full teaching day. The 2026 Semester 2 break is 7–20 September; the week of the 21st is week 7, with lectures, labs and an assessed reflection task. Subtract a day from a "Return from" date before treating it as an end. - **If the timetable and your paired calendar range disagree, you have paired it wrongly.** The timetable is generated from the teaching schedule and knows about breaks. Scheduled classes sitting inside what you computed as a break is the symptom of the off-by-one above, not a conflict to resolve in the calendar's favour. Never advise anyone to skip a scheduled class on the strength of a range you paired yourself. - **The wording of the pairs drifts.** A break "commences" and you "Return from" it — the marker is at the *front* of the closing summary. The Semester 1 examination period has no "begins" at all, only an "ends". A deferred period is "… period 1 begins (two week duration)", with the marker before a parenthetical. And one pair in 2026 opens with a hyphen and closes with an en dash. Match on keywords, and normalise before comparing. - **`DTSTART` is `T000000Z`** although the date meant is the Canberra local one. Read the date part only. Converting the timezone moves Semester 1 census day from 31 March to the 30th. - HTML entities appear in `SUMMARY` (`'`); unescape them. - The `audience` filter is unreliable — `?audience=` on the feed returns all events regardless. Filter yourself. - Page metadata reports `modified_time` 2024-10-14 even for 2027 content, so it tells you nothing about freshness. Cross-check any date you will rely on against the HTML page, and cite that page. - These are university-wide dates. Course assessment dates are in the class summaries; school deadlines (curriculum rounds, class summary due dates) are published somewhere else entirely. ## Network These are the hosts involved, for an egress allow-list: ``` programsandcourses.anu.edu.au programs, courses, class summaries policies.anu.edu.au the Policy Library mytimetable.anu.edu.au the class timetable exams.anu.edu.au the exam timetable www.anu.edu.au the university calendar, the legislation index api.prod.legislation.gov.au Register metadata www.legislation.gov.au Register document text ``` A 403 carrying an `x-deny-reason` header is your own gateway, not ANU, and no header or proxy change gets around it. `anu-pandc` says which of the two refused you.