Home / The map, and the review
Phase 0 and phase 3 of the move
The map, and the review
Before anything moved, everything a reader does on newsroom.sgit.ai was mapped: every file, every control a reader can click, type or pay, every input, every place data leaves the browser, every contract. Then it was reviewed line by line. The map is regenerated from the tree; this is the one from newsroom v0.12.50, the before of the move, and the review's eighteen findings are this repository's first eighteen issues.
The map as a page: newsroom.sgit.ai/newsroom-api/; as data: map.json.
How the review reads the code
Source code is layers within layers, each a graph with its own vocabulary, and a review should read a change at every one: that is the method in Code review as a fractal semantic graph. Here the layers are: the site a reader opens; the pages written for the reader code; the views, controls, inputs and outputs on them; the files that run them and the contracts between them; and inside each file the functions the findings name. The map as a graph draws those layers and the edges between them, and every finding is anchored to a node.
The findings
Severity first, then by number. Each links to its issue; pinned by names the test that records the behaviour as it is today, so that the fix later is a visible change to a test and not a silent one (the brief's rule 3).
| Finding | Severity | Area | What | Pinned by | Proposed change |
|---|---|---|---|---|---|
| R10 | high for the move | Content-Security-Policy | Every imported page loads assets/site.js, subscribe.js and shots.js by fetching the text and running it with (0, eval)(text) (the loader's bridge path), and the loader and the meter config are inline. A script-src policy without 'unsafe-eval' and without hashes breaks every page. frame-ancestors cannot be set in a meta tag at all, and GitHub Pages sets no headers, so framing (the Keep, Remove and Top up buttons under another site) is not prevented today. | test_review.mjs R10 (counts the pages that eval) | load scripts with <script src> or import(); then a meta CSP for script-src and connect-src; frame-ancestors needs a host that sets headers |
| R11 | high per the brief | the pipeline | The one workflow ran validate.js only; the browser tests (46 + 65 checks) never ran in CI. | fixed in this release: a reader-tests job runs all three test files before the tag | done; the move's pipeline adds WebKit and Firefox and a pre-production copy |
| R1 | medium | the meter's logic | Declining a page as “not relevant” leaves it in the persona's reading list. open() auto-keeps an article on first open; decline() refunds the charge but does not undo the auto-keep, so the article stays under “Its reading list” and profile() adds +5 for it while the decline subtracts 2: a page the reader rejected pulls its topics up. | test_review.mjs R1 (passes while the article stays in the list) | decline() removes an autoKept entry from the active persona's on[]; a reader who added it by hand keeps it |
| R4 | medium | data correctness | A reading share or a subscription sent from this site is labelled sgit.ai: the imported config has share.site = "sgit.ai" (so the text begins “My reading on sgit.ai”), subscribe.js writes From: site@sgit.ai, and the contact file says site: sgit.ai. The agent that drains the lane can tell the two sites apart only by the Sent from line. | test_review.mjs R4 | a recorded meter rule for share.site (admin/build/newsroom-meter.json); subscribe.js and the contact file are verbatim imports and need this site's own copies, or a parameter, after the move |
| R9 | medium | the number contract | If sgit.ai renames an article (a new slug), ids.py marks the old number withdrawn and gives the article a new number. A reading list that named the old number then says the article is no longer published, although it is. | described; a rename has not happened yet | a renamed_to field on the old entry, followed by the list page; the gate requires it when a slug disappears and a new one appears in the same import |
| R2 | low | restore from a file | restore() checks that the file has a numeric balance and a log array and nothing else. A persona without on[] and off[] makes the personas and newsroom views throw and show nothing; a log entry's path is rendered as the history link's href as it is; a persona's theme is written into a style attribute as it is. The file is the reader's own, so the exposure is a file someone else sent them to restore. | test_review.mjs R2 (the blank personas page); the href and theme cases are described, not pinned | normalise on read: personas get arrays, paths must be same-origin paths, themes must be a hex colour; unknown fields dropped |
| R3 | low | the return page | A malformed percent sequence in ?session_id= (for example %E0%A4%A) makes decodeURIComponent throw inside onReady, so the element fails with “did not load: URI malformed” instead of saying the reference is missing. | test_review.mjs R3 | try/catch around the decode; treat an undecodable id as no id |
| R6 | low | display | Money is kept to a tenth of a penny and shown to a penny. Balance and spent are each formatted on their own, so a half-penny state shows, from £5.00, a balance of £4.95 and £0.06 spent, which do not add up; the history shows 5.5p. A reader who checks the arithmetic finds a penny missing. | test_review.mjs R6 | one rounding rule for the pair (format spent as start − balance, or keep both to 0.1p on the account page) |
| R8 | low | robustness | feed() caches a failed fetch as null for the life of the page, so one transient failure leaves “The list of articles could not be loaded” on every view until a reload. | test_review.mjs R8 | do not cache a failure; retry on the next call |
| R14 | low | bounds | sessions[] and topups[] grow without bound, and a payment reference is not checked for shape before it is stored. | described | cap both; keep references that look like cs_… only |
| R15 | low | personas | persona(id) falls back to personas[0], which is “me” only when “me” is first; after a restore that puts another persona first, an unknown id resolves to that persona. | described | fall back to the persona with id me |
| R5 | note | operations | On the live site the lane for shares and subscriptions is on dev.send.sgraph.ai and the study page's vault frames come from dev.vault.sgraph.ai. Both hosts are named “dev”. Production reader data goes to them. | test_review.mjs R5 (records the hosts) | a decision, not a code change: either these are the production hosts and say so, or the files point at production ones |
| R7 | question | picks | picks() excludes every page in the log, whichever persona read it, while profile() is per persona. An article read as Morgan is never picked for Rowan. | test_review.mjs R7 (records the behaviour) | decide: per-persona exclusion, or keep and say so on the newsroom page |
| R12 | note for the move | two upstreams | The account pages, subscribe, the contact file and the component base are imported from sgit.ai on every sync and cannot be changed here except by a recorded rule, and verbatim files (scripts, JSON) cannot be changed by rule at all. R4 is the first consequence. The newsroom's meter is v1.4.0 while sgit.ai's is v1.3.1. | the brief's phase 1 removes it | the move: both sites pin released files |
| R13 | note | what leaves | The export includes the payment references (sessions, topups[].ref) that the share deliberately leaves out. A reader who sends their export instead of their share sends them. | described | say so beside the button, or export without sessions |
| R16 | process | caching | Imported pages load the meter as sg-meter.js?v=<sgit.ai's release>, so the cache-buster never changes for a release of this site. That is fine while a published version is never edited in place, which is the rule; it is written here because v1.4.0 was edited in place while it was being built. | described | a change is a new version path, always |
| R17 | note | reading lists | A list's persona is keyed by a 32-bit hash of the names and numbers, so two different lists can share one persona with probability about one in four billion per pair. | described | a longer hash, or the fragment itself as the key |
| R18 | note | listeners | The landing view adds a window hashchange listener per element and never removes it, and re-renders on every meter change (the picker's “Copied” label resets). | described | one listener, removed on disconnect |
Accepted, and said so
Gaps the design accepts, each written where a reader can find it.
| Gap | Why | Where |
|---|---|---|
| The return page credits any session id | a static page cannot verify a payment; building the address by hand credits the reader's own screen | meter/security.html |
| The reader can edit their own balance | the state is in their storage; nothing is behind a wall | meter/security.html, No server by design |
| The names in a reading list are whatever the link says | there is no signature yet; a signed list is a later step on RFC 0001's keys | for/index.html |
| A page is charged by the deepest point reached, and the reader card is at the foot | reaching the card to rate a page is reading to the end | going-live-with-the-reading-meter.html |
Where the move left things
- The proof of the move: the same bytes, before and after.
- State, inputs and outputs, from the same map.
- The contracts.