api.newsroom.sgit.ai v0.2.14

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.

reader code9,903 linesin 62 files that run in a browser
frozen5,697 linesin 20 files of earlier meter versions, served and never changed
surface56 controls12 views on 29 pages; 11 inputs, 9 outputs
contracts14formats other sites and agents program against
findings18each an issue here, each pinned by a test where it can be
tests491 checksin eleven files, run in three engines on every commit

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).

FindingSeverityAreaWhatPinned byProposed change
R10high for the moveContent-Security-PolicyEvery 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
R11high per the briefthe pipelineThe 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 tagdone; the move's pipeline adds WebKit and Firefox and a pre-production copy
R1mediumthe meter's logicDeclining 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
R4mediumdata correctnessA 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 R4a 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
R9mediumthe number contractIf 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 yeta 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
R2lowrestore from a filerestore() 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 pinnednormalise on read: personas get arrays, paths must be same-origin paths, themes must be a hex colour; unknown fields dropped
R3lowthe return pageA 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 R3try/catch around the decode; treat an undecodable id as no id
R6lowdisplayMoney 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 R6one rounding rule for the pair (format spent as start − balance, or keep both to 0.1p on the account page)
R8lowrobustnessfeed() 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 R8do not cache a failure; retry on the next call
R14lowboundssessions[] and topups[] grow without bound, and a payment reference is not checked for shape before it is stored.describedcap both; keep references that look like cs_… only
R15lowpersonaspersona(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.describedfall back to the persona with id me
R5noteoperationsOn 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
R7questionpickspicks() 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
R12note for the movetwo upstreamsThe 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 itthe move: both sites pin released files
R13notewhat leavesThe 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.describedsay so beside the button, or export without sessions
R16processcachingImported 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.describeda change is a new version path, always
R17notereading listsA 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.describeda longer hash, or the fragment itself as the key
R18notelistenersThe 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).describedone listener, removed on disconnect

Accepted, and said so

Gaps the design accepts, each written where a reader can find it.

GapWhyWhere
The return page credits any session ida static page cannot verify a payment; building the address by hand credits the reader's own screenmeter/security.html
The reader can edit their own balancethe state is in their storage; nothing is behind a wallmeter/security.html, No server by design
The names in a reading list are whatever the link saysthere is no signature yet; a signed list is a later step on RFC 0001's keysfor/index.html
A page is charged by the deepest point reached, and the reader card is at the footreaching the card to rate a page is reading to the endgoing-live-with-the-reading-meter.html

Where the move left things