| v0.23.6 | 2026-09-18 | The four questions move to the top of the page, where a thumb can reach them. This page gets opened one-handed, in a hall, thirty seconds before a conversation starts — and the four fixed questions were the third pack down, behind two packs about ChatGPT interviews that nobody is reading while a founder waits. They are now the first thing on it: numbered, English and Portuguese stacked so the interviewer reads whichever the person in front of them speaks, each with the one line saying why it is asked that way. The closing question and the thirty-second read-back sit under them. They are rendered from one file, not retyped into a second place. 00__the-spine.json holds the four questions and the closing one; the top card renders from it and the two question cards keep their long-form treatment. Four sentences are a small thing to copy and exactly the kind of small thing that drifts — one gets sharpened in the card, the card is what gets read at the event, and the page at the top quietly goes on saying the old version. Gate 43 makes that impossible. Every question in the spine must appear verbatim in both question cards — English in the English card, Portuguese in the Portuguese one — and on the rendered page. Gate 42 already tied the cards to the page; this ties the spine to the cards, so the chain is closed at both ends. Broken on purpose in both directions before it was trusted: a question edited in the spine and not in the card went red naming the card, and a question edited in the card and not in the spine went red twice — once from gate 42 for the stale hash, once from gate 43 for the drift. And the language gate caught this gate's own section heading, which I had written in Portuguese inside an English file. admin/versions/v0.23.6.md — this note as markdown, which is what it is. |
| v0.23.5 | 2026-09-18 | A question card for interviews you conduct yourself, at an event, with a recorder running. The third interview pack, and the only one where a human does the interviewing. Pack 09 assumes half an hour and a quiet room; a summit gives you five minutes, standing, in a hall with a PA system, with somebody who has a meeting at the top of the hour. Different constraints, different instrument. Four fixed questions, asked identically to everybody. 1. In one sentence, what do you make, and who pays for it? 2. What can you do today that you couldn't do eighteen months ago? 3. What's the hard part — technology, money, or people? (make them pick one) 4. What does everyone say about AI in Portugal that doesn't match what you see? Improvising the spine feels better in the moment and destroys the comparison. Asked identically, twenty conversations become something you can count — how the hard part splits across twenty founders, what capability actually changed in eighteen months, whether they disagree with the consensus about the same thing. Counting is the one thing this newsroom can do that a blog cannot. And the closing question, which is the strongest thing in the pack: "who else here should I be talking to, and why them?" It draws a map of the room from the inside, and it is the question easiest to skip at four in the afternoon. The card exists in English and in Portuguese, and it renders at 390px because it will be read on a phone in a hall, which the browser gate checks. The first content on this site born in one language and published in another. The recording is in English; the site publishes European Portuguese. The pack proposes the rule and empresas/issue 009 asks the editor to make it one: - The transcript is never translated. It is evidence, and evidence is not translated — the same rule that keeps a frozen page's bytes untouched. - A translated quotation carries its original, or it stops being a quotation and becomes a summary in our words, unquoted. This publication's whole claim is that what sits between quotation marks is what was said. - The interviewee approves the Portuguese, not only the English. They are being quoted in a language they may not have chosen. - The article says, in its provenance, which language the conversation was in. There is no ninth section, and that was not a choice. The editor asked for a "Portuguese Startups" section; CLAUDE.md fixes the brief's eight and is deny-listed. A startup is a company and empresas is the brief's section for companies — so the interviews land there as a series, which is a shape inside a section rather than a section of its own. Issue 009 carries the naming decision, and notes that a series name is read by a reader and is therefore Portuguese. What a founder says about their own company is a primary source. What they say about the market is a lead to verify, exactly like a line in a research delivery — a number said confidently at a summit is still a number with no source behind it. admin/versions/v0.23.5.md — this note as markdown, which is what it is. |
| v0.23.4 | 2026-09-15 | The operations console moves to /newsroom/, which is where the editor says every admin action belongs. Eleven pages — the console index, mail, board, bridges, team, agent activity, design review, guidance and documents, plus the six guidance documents — left /backoffice/. Their builders went with them: build/backoffice.py and its three siblings are now build/newsroom.py, newsroom_bridges.py, newsroom_design.py, newsroom_team.py. The navigation label a reader sees changed from Back office (EN) to Newsroom (EN). The interviews page became a console page rather than moving as-is. It had been built with the newspaper's chrome and simply relocated, and gate 19 caught that immediately: any page in the console namespace must declare lang="en" and carry the notice saying this is not the publication. The gate was right and the page was wrong — it is about how this newsroom works, not about what it found, which is the same test that put the other eleven there. It is now /newsroom/interviews.html, built by the console's own shell, in the console's rail under Reference. What is left under /admin/, and why each thing is still there. The question was asked directly, so here is the whole of it: | What | Why it has not moved | |---|---| | admin/review/index.html | Gate 13 permits unapproved delivery items to be shown with their state on /entregas/, /redacao/ and /admin/ only. One line in build/gates.py adds /newsroom/. Deny-listed. | | admin/versions.html | admin/build/validate.js reads this exact path to check the release table has one row for the current version. Deny-listed. | | admin/build/version.txt | Same file, line 54, same problem. It is also read by CI and by before_push.py. | | admin/build/validate.js, render.mjs | The toolchain, not an admin action on the site. It can stay; moving it means editing CI and the deny-listed file that names itself. | So /admin/ survives on three lines in two files that no agent here may edit — and that is the design working, not failing. An agent that can edit the gate that stops it has no gate. Issue 008 carries the exact changes; none of them was worked around by reaching for Bash, which would be the same edit under another name. The browser gate earned its keep again. The interview prompts rendered as <pre> blocks with no wrapping rule — .bo-pre was a class I invented and never styled — and the page came out 937px wide in a 390px viewport. No other gate looks at width. A prompt that needs sideways scrolling on a phone is a prompt nobody copies on a phone, and copying is the entire point of that page. Old release notes that linked into /backoffice/ were repointed, because the site gate requires every link to resolve. Their prose was left alone: a note saying a page was at /backoffice/… in March was true in March, and rewriting the record to match today would be the one edit this repository never makes. admin/versions/v0.23.4.md — this note as markdown, which is what it is. |
| v0.23.3 | 2026-09-15 | The editor was right: the operational surface was still speaking Portuguese in its addresses. /redacao/revisao/ and /redacao/entrevistas/ were pages about how the newsroom works, not about what it found, and the one test that decides these questions — would a visitor read this string on the site? — says English. The map drawn in v0.22.0 already agreed and nobody had noticed: redacao/ is not in its list of reader URLs that stay Portuguese, and its subfolders are already mapped to newsroom/. What moved. /redacao/entrevistas/ → /newsroom/interviews/. build/revisao.py → build/review.py, build/entrevistas.py → build/interviews.py, dados/revisao.json → dados/review.json, dados/entrevistas.json → dados/interviews.json, the pt-decisoes component → pt-decisions, briefs/pack/09__entrevistas/ → 09__interviews/ and 10__entrevista-ao-editor/ → 10__editor-interview/, with English filenames inside both. Every key in the two data files, every identifier in the component, and the prose of both pages. The quoted evidence stays Portuguese — a claim, an excerpt, an article title and a decision comment are values, and the map renames vocabulary and never content. So do the two .pt.md interview prompts: they are pasted into a Portuguese conversation by a Portuguese speaker. And one thing did not move, which is the honest part of this release. The editor's review page went to /admin/review/ and not to /newsroom/review/. Gate 13 — the delivery quarantine — permits unapproved research items to be shown with their state on exactly three surfaces: /entregas/, /redacao/ and /admin/. This page is that kind of surface and shows exactly that, so /admin/ is the exemption used for its stated purpose rather than worked around. The right address is /newsroom/review/, and reaching it needs one line added to a list inside build/gates.py — which is deny-listed, because an agent that can edit the gate that stops it has no gate. That line is the editor's, and issue 008 asks for it, with the exact change. What was not done, and was considered. The page could have been made to pass at /newsroom/review/ by not printing each item's headline, since that is the string gate 13 matches on. That would have been gaming the gate: the headline is the question the editor is being asked, and a page that hides what it is asking about to satisfy a check is worse than a page at a second-best address. If the place is not permitted, move the place or change the list — never change what the page says. Rewritten rather than find-and-replaced. Halfway through, a bulk rename produced save(estado), this.items = itens, and a builder calling a variable that no longer existed — exactly the half-translated identifiers v0.22.0's dry run found 120 of. Both builders and the component were rewritten from scratch instead. A repository-wide rename reads like a find-and-replace and is not one. Renames are recorded in dados/en-migration.json under done_since_the_map, so the next session sees what was already done and why the one exception exists. admin/versions/v0.23.3.md — this note as markdown, which is what it is. |
| v0.23.2 | 2026-09-15 | The release rules stop living in one session's head. Everything this week's work learned the hard way is now a guidance page, releasing.md, rendered at /backoffice/guidance/releasing.html and linked from .claude/ONBOARDING.md — which is the file a session is pointed at on startup, and therefore the only place a lesson reliably reaches the next agent. Which component is the minor, stated once and for good. The version is v<release>.<major>.<minor> and the minor is the third. Every push to dev is a minor release, so a normal one goes v0.23.0 → v0.23.1. CI is the authority and agrees: its tag job computes NEXT_MINOR as the third component plus one and rejects anything that is neither that nor a deliberate major. build/before_push.py advises the major and is wrong about it — it is reliable for whether a number is taken, not for which to take. That disagreement is now written down, in the guidance and in the onboarding file, instead of being rediscovered by whoever releases next. And the three other things that fail quietly. Install requirements.txt first: without pycryptodomex an AES-encrypted diploma from the gazette reads as zero characters and is reported as «provavelmente um PDF digitalizado», a false statement about somebody else's document; without jsonschema a research delivery is not validated at all. Both leave every gate green. How to run the browser gate in a container that has no Playwright, with the exact commands, since installing it into the repository turns the site gate red. And the two merge commands that delete work without raising a conflict — git checkout --theirs over the unmerged list, which overwrites resolutions written to disk but not staged, and a clean auto-merge, after which you grep your own changes back one at a time. The editor gets interviewed too. briefs/pack/10__entrevista-ao-editor/ is 09__entrevistas/ pointed the other way: instead of an assistant interviewing somebody outside the newsroom for material, an agent has ChatGPT interview the editor of record for direction — what to work on, what to drop, what he thinks matters. An agent knows what it has in front of it and does not know what the editor thinks is important; that information exists and today only surfaces when he remembers to write it down. The part that makes it worth twenty minutes is that the agent reads itself first. Before there is any interview, the agent runs the preparation prompt over its own ROLE.md, MANDATE.md, open and blocked issues, unread mail, run records, and the release notes describing defects in its own domain — an agent that does not know what it broke repeats it. Out of that comes a state block carrying its real open questions, each with its own proposal. The difference between "what should I do next?" and "issue 006 has been stalled three days waiting on a PDF I cannot find; I see three routes and I would take the third" is the difference between a polite conversation and a decision in ten seconds. Decisions and ideas come back in separate sections, and are treated differently. A decision becomes a file, with the editor's reason quoted verbatim, because the reason is what lets the next similar case be decided without asking again. An idea opens an issue in proposto and nobody starts it — an idea treated as an order is how an agent does work nobody asked for. Anything the editor asserts about the world is a lead to verify like any other; his saying a name is not a source. And nothing said aloud to a model puts a story into publicado, widens the folder an agent may write in, or excuses a gate. Both packs render on one page. /redacao/entrevistas/ now carries eight prompts in two groups, each from its markdown source, still under gate 42 — which was written for one pack and covers the second without a change, because it checks the relationship and not the list. admin/versions/v0.23.2.md — this note as markdown, which is what it is. |
| v0.23.1 | 2026-09-15 | The editor gets a page that asks him the questions, instead of six pages he has to read. Coming back after a day away meant reconstructing, from the release notes, the delivery pages, the board, the mail, the revision files and the articles, what had happened and what was now his to decide. Every one of those is honest and none of them is a briefing. /redacao/revisao/ gathers the open questions — and only the ones whose answer is the editor's to give — with the file each answer has to end up in and the agent that will put it there. Today: six research items, one blocked issue, one ontology proposal. It captures answers and it writes nothing, and it says so on the page. A verdict, a comment and a reaction per question, kept in localStorage so a review can be interrupted and resumed, and then handed back as a block of text addressed to the agent that will do the work — copy all, or copy just what goes to one agent. This site is static and cannot write to git, and a console that looked like it recorded a decision while recording nothing would be the one dishonest page on a publication whose whole argument is that nothing is asserted without bytes behind it. The browser holds the editor's words; an agent turns them into files; nothing is a decision until the file exists. A question with no verdict stays por rever, which is what the quarantine gate already enforces — so the safe state and the default state are the same state. And the page does not pretend to know when your last review was. It lists when each thing happened and leaves "have I seen this?" to the browser, because that is knowledge about one person on one device, not a fact about the newsroom. The interview method, and the prompts that do it. briefs/pack/09__entrevistas/ is the other half of what this newsroom publishes: 08__research-briefs/ sends an assistant after documents, this sends it into a conversation. Four pieces — the interviewer prompt in Portuguese and in English, the topic block that is the only part that changes, and the prompt an agent is given to assemble a pack for one named person. A person too busy to write two pages about their work will talk for twenty minutes about it, at an hour that suits them, by voice or by typing. The rules do not bend for it: an interview is a primary source about the person speaking and nothing else, so what they assert about the world is a claim to verify and not a fact for having been said aloud; three fields per person as everywhere else on this site; no contact detail in any file; the interviewee approves the report and the editor of record decides publication, two decisions neither of which substitutes for the other; and removal on request stays unconditional and carries no reason. /redacao/entrevistas/ renders those prompts rather than linking them, and gate 42 makes that safe. A prompt is copied, not read — by someone about to paste it into ChatGPT, often on a phone — and sending them to a raw markdown file to select-all is how you lose them. So the page is a second VIEW of one copy: rendered from the markdown on every build, marked derived, and checked. A derived file with no gate is a second copy with extra steps, which is what gate 28 learned from the agent mandates. Gate 42 was broken on purpose in both directions before it was trusted — a hand-edited page and a source changed without a rebuild — and its first version passed the hand-edited page, because it sampled a single line. A gate that checks one line certifies one line. It now samples across the whole file.
One latent bug, found by the browser gate. The entity linker turns a name's first mention into a link, and it protected <a> and <code> but not <script>. The review page carries its questions in an inline <script type="application/json">, and the linker reached inside and rewrote entity names there into <a class="ent" href="…"> — well-formed HTML, and JSON with unescaped quotes in the middle of a string, so the component failed to parse it and declared itself in error. No other gate could have caught it: the page was valid, the damage was inside a string, and only opening it in a browser showed anything wrong. The linker now skips <script> and <style>, which is what it should always have done for any page carrying inline data. Numbered v0.23.1 and not v0.24.0: every push to dev is a minor release, and the minor is the third component. build/before_push.py says to take the next second component, which disagrees with CLAUDE.md and with what CI's tag job computes as NEXT_MINOR; the editor's instruction settles it, and the tool's advice is noted for whoever owns that file. admin/versions/v0.23.1.md — this note as markdown, which is what it is. |
| v0.23.0 | 2026-09-15 | The messages now say where things actually are. v0.20.0 renamed the back office's addresses to English and deliberately left redacao/correio/ alone, on the grounds that a message is immutable in Email-FS-lite. The editor overruled that, and was right: "email-fs-lite is convention-based and it is to make changes like this, especially at such an early stage — since leaving it is worse due to the technical debt that it will create." So four messages — each in both of its copies, the sender's in saida/ and the recipient's in expedicao/ — plus three run records now name board.html, bridges.html, design.html and the renamed generators instead of addresses that stopped existing three releases ago. The interesting part is not the sweep, it is that the rule had to be rewritten to allow it. Immutability here was stated flatly in two places — redacao/correio/LEIA-ME.md and the mail page — and this repository's whole argument is that a rule the record quietly contradicts is worse than no rule at all. A commit that edited four "immutable" messages while both of those still said "never edited" would have been exactly that. redacao/correio/LEIA-ME.md therefore carries the exception, its conditions and its log:
- Immutability here is a discipline, not a mechanism. Nothing in a git repository stops a write. The protocol is convention-based, so the convention can be changed — and if it is, it has to be said. - Only the editor of record authorises a sweep, and the authorisation goes in the commit subject. - A sweep is mechanical and uniform, and must not change what any message asserts. Swapping a renamed path: yes. Rewriting a sentence, correcting a number, softening a conclusion: never — that is what a reply and In-Reply-To are for. - Both copies move together. A message lives in the sender's saida/ and the recipient's expedicao/ or entrada/, and the two must stay byte-identical. Editing one is how the model actually breaks; both pairs were diffed after this sweep and match. - The log is a table, with what each sweep changed and what authorised it. This is its first and only row. And the regime change is written down rather than left to be rediscovered: this is cheap now because nothing outside the repository cites these messages. The day one of them is quoted from outside, a sweep acquires a cost it does not have today, and that section is where that gets recorded. The mail page carries the same qualification, and deliberately does not print the version the last sweep happened in — it would have been rendered from version.txt, which means the page would have claimed the last sweep happened in whatever release you happen to be reading. The protocol note holds the number; the page points at the note. Content exists once. Also in this release: nothing else. The sweep, the rule it needed, and this note. admin/versions/v0.23.0.md — this note as markdown, which is what it is. |
| v0.22.0 | 2026-09-15 | The editor's point is right and larger than it looks. dados/ is Portuguese leaking into the backend, and with more language editions coming the data layer has to be language-neutral: a Portuguese key is fine while there is one edition and wrong the moment there are two. This release does the preparation, proves it, and stops at a wall it may not climb. dados/en-migration.json is the map, and it is data — because this repository's rule is that a classification is a published formula or it does not happen. 467 keys and 8 folder renames, and, as importantly, what it does not touch, each with a reason: reader URLs (artigos/, entidades/, empresas/ — in a multilingual future those are the Portuguese edition's paths), the ontology's verbs (rule 6: the graph reads aloud in Portuguese), the entity type names that appear in those URLs, and every value in every file. The map renames the vocabulary, never the content.
build/migrate_to_english.py applies it, and refuses to. Dry run by default; --apply checks the blockers first, prints what the editor has to change, and exits non-zero rather than doing four-fifths of a rename.
## What running it four times against a scratch clone found A repository-wide rename reads like a find-and-replace. It is not, and the only way to learn that cheaply was to do it somewhere it did not matter. Six classes of defect, every one of which would have reached the live site: 1. Folder renames applied child-before-parent. Renaming fontes/congeladas first creates sources/, so the later fontes → sources is skipped as "target exists" — leaving an empty fontes/ and the move half-done. 2. The pattern matched "dados" but not "dados/historias.json", nor "fontes/congeladas" with no trailing slash. The second is how nearly every builder refers to a folder. 3. The key map was built from dados/ alone. Keys that live only in artigos/**/comentarios.json were renamed in the code and not in the data. 4. 120 keys came out half-translated. porque_existe_este_ficheiro → why_existe_este_file; nao_e_aconselhamento_juridico → not_and_aconselhamento_juridico. They are phrases, not compounds. All 120 are now written by hand, and a rule that got those wrong is not trusted with the other 469 unchecked either. 5. redacao/ is a reader URL, not a backend folder. redacao/index.html is the newsroom-floor page. Its subfolders move to newsroom/; the page stays. Found because the folder survived the migration holding exactly one file, and that file was a published page. 6. Portuguese Python identifiers are invisible to a string-level rename. junta(agente=…, especie=…, porque=…) builds a dict whose keys the map renames — so the function is handed agente and reads agent. 126 of them in build/. Unsolved, and the largest piece left; it is code-only, and needs Python's own parser rather than a regular expression. ## Why it stops here build/gates.py and build/entregas.py hardcode the backend folder names and 115 of the keys, and both are in the deny list of .claude/settings.json. An agent that can edit the gate that stops it has no gate at all — so no agent working here may touch them, and renaming around them turns the build permanently red through files no agent here can fix. dados/aviso.json, also deny-listed, holds 33 more.
That is a permissions decision, and it belongs to the editor of record. The note is in redacao/correio/editor/entrada/, with the two ways out and what each costs. A run that stops honestly is a good run. Nothing was renamed in this release. The map, the script, the editor's note and this page are the whole of it — and docs/guidance/language.md, which has said since v0.10.0 that this migration must happen alone in one release, now links to the map and says how big "alone" turned out to be. admin/versions/v0.22.0.md — this note as markdown, which is what it is. |
| v0.21.0 | 2026-09-15 | A session starting work on this repository now lands on something. There was no briefing: Claude Code loads CLAUDE.md and nothing else, the guidance existed only as markdown in docs/guidance/, and the one route to it on the site was /backoffice/docs.html#docs/guidance/language.md — a fragment, which is not an address. It cannot be cited in a commit message, it is not in the sitemap, and anything fetching it gets the shell of a document browser rather than the document. llms.txt, the file this site tells machines to read first, mentioned the guidance zero times — an agent arriving to change the site found no route to the rules it was about to break. Every guidance document now has a page of its own, at /backoffice/guidance/<name>.html, rendered from the markdown by build/guia.py on every build. They are in the back office because that is where English pages live and where gate 19 already checks they carry lang="en" and the box saying they are not the publication. llms.txt gains a section pointing at all five — in English, unlike the rest of that file, and it says why in the file itself. .claude/ONBOARDING.md is the short form, and build/briefing.py prints it from the SessionStart hook, so it reaches every session without being asked. It reads the version, the next free gate number, the agent register and the last run record off disk rather than restating them — a briefing with a number typed into it goes stale on the day it matters most. The permissions block of .claude/settings.json was not touched, and the change asserts that rather than promising it: CLAUDE.md forbids a run from widening its own permissions, and a hook is not a permission.
Gate 41 keeps it true: every document under docs/guidance/ has a page, llms.txt points at them, and every link in the briefing resolves. A briefing whose links have rotted is worse than no briefing, because it reads as current. Three bugs in the shared inline renderer, found by rendering prose that had never been rendered. Backticks were not handled at all, so every like this reached the page as literal backticks — invisible on an article, which rarely uses them, and everywhere on guidance pages, which are mostly file names. Because nothing protected a code span, a [[fonte:…]] written inside one, the syntax being discussed, became a live chip pointing into the register at an id that does not exist: a page explaining how a citation works was making one. And *italics* came through as asterisks. Fixed in paginas.inline() for every page on this site, not worked around in the new renderer. Gate 19 got narrower, which made it more accurate. It failed the guidance page for carrying a source mark — correctly, by its old reading. A mark inside <code> is the syntax being quoted, not a citation being made, so it is now skipped; an unquoted mark still fails, and the check that matters — a back-office page linking into the register — was never in question. Verified by injection: an unquoted [[fonte:dados-gov]] on a console page still turns the build red. Markdown links were rendering for the first time, and the site gate immediately found nine broken ones — links that had been correct between two markdown files and were wrong the moment one of them became a page two directories away. One, llms.txt in index.md, had simply been wrong since it was written and nothing had ever rendered it. And the gate that catches what this release is about caught this release. Gate 36 — added four versions ago and never yet fired in anger — went red on the merge: another session had taken 39 in build/gates_desenho.py while this one took 39 in build/gates_artigos.py. Theirs shipped first, so this one renumbered; then 40 was taken too, in admin/build/render.mjs, because that session had extended gate 36's own registry to cover the browser gate and the site gate — a registry covering some of the addresses is one that hands out addresses twice. This gate is 41. --- Three things found on the way that were nobody's intent. The browser gate caught two pages that drag sideways on a phone, both from the console rewrite two releases ago — it is a local check, not a CI one, so a session that does not run it ships without knowing. On the board, a status pill carrying a whole sentence («blocked on build/pdf.py está na lista de recusa; nenhum agente…») was white-space: nowrap and 538px wide on a 390px screen. On the mail page it was three pixels, from a URL long enough to have nowhere to break — and three pixels is enough, because the reader still has to pull the page back to read the next line. The back office is mostly paths and URLs, so long words now wrap there by rule rather than by luck. Two article folders were left as orphan pages. uma-captura-nao-mostra-movimento and registo-nacional-nao-devolve-texto had their artigo.json, artigo.md, claims and provenance removed on the release branch when the article set was replaced, but their generated index.html survived this merge — two pages on the live site with a stale version badge and every link inside them broken. They are gone. The article gates could not have seen this: gate 16 walks artigos/**/artigo.json, so a folder with no artigo.json is invisible to it, and the derived index had already dropped both. The site gate found it, by following the links. The min-width: auto trap, twice in one afternoon. A flex or grid child refuses to shrink below its content unless min-width: 0 is set, so no amount of wrapping downstream helps until it is. It is the load-bearing half of the status-pill fix, and it is worth knowing before the next thing on this site overflows. Renumbered twice on the way out.* Another session released v0.20.0 while this note was being written, and had written its own admin/versions/v0.20.0.md; the same merge also turned up backoffice/viewer.html, a page whose generator they deleted in v0.17.0 and which this side's build had faithfully recreated — the third orphan of the day, and the same shape as the two article folders. And a release note from v0.16.0 pointed at /backoffice/desenho.html, renamed to design.html in their v0.20.0: historical notes keep their prose and get a working address, since it is the same page. **build/before_push.py named the collision before the push rather than after it, which is the whole of what it is for. admin/versions/v0.21.0.md — this note as markdown, which is what it is. |
| v0.20.0 | 2026-09-15 | The back office's addresses are English now, and nothing was left behind to say so. The editor settled the vault's open question 2: this site has no readers and no SEO yet, it is still in experimentation, so an address may simply stop existing — and what they explicitly did not want was a scatter of files whose only content is "this moved". | was | is | |---|---| | backoffice/correio.html | backoffice/mail.html | | backoffice/quadro.html | backoffice/board.html | | backoffice/equipa.html | backoffice/team.html | | backoffice/pontes.html | backoffice/bridges.html | | backoffice/desenho.html | backoffice/design.html | | backoffice/viewer.html | deleted | viewer.html is the one that makes the point. It had been kept since v0.3.1 as a permalink that forwarded to docs.html, carrying the #fragment across — a page whose entire content was the sentence "this page moved into the document browser". That is precisely the stub this release exists to not create five more of, so it is deleted rather than renamed. The rule it protected is untouched: docs.html is still the one and only implementation of document rendering here.
index.html is deliberately NOT renamed to console.html, which is what the proposal asked for. /backoffice/ is the address the rail links, the paper's footer links and gate 19 requires; a directory wants an index; and a console.html sitting beside an index.html that both had to exist would be the stub problem again, one directory down.
The generators took English names too, because a module called equipa.py that writes team.html is the same drift one level below the URL: build/equipa.py → build/backoffice_team.py (team, board, mail) build/pontes.py → build/backoffice_bridges.py build/desenho.py → build/backoffice_design.py and the build lines they print are in English with the rest of the code. The line drawn, and worth stating because it will come up again: the paper's machinery and data stay Portuguese — artigos.py, entidades.py, gates_desenho.py, dados/historias.json — because the paper is Portuguese and that is internally consistent. It is the back office's pages and the modules that write them that are English, because the back office is. What was NOT rewritten, on purpose. Message bodies under redacao/correio/ and the run records still name the old addresses, and they still will. A message is immutable in this protocol — never edited, never deleted, only moved — and a run record says what happened; rewriting either to match today's tree would be falsifying a record to tidy a search result. Both mention the paths in prose rather than as links, so nothing dangles. Old release notes stay as written for the same reason: v0.16.0's note describes v0.16.0, where desenho.html was the correct address. Gate 39 caught itself, which is the interesting part. The ratchet on the console's legacy layer held a hand-written list of four generator filenames. Three of them were renamed in this release, so the gate went looking for build/equipa.py, found nothing, and reported 1 of 4 instead of 4 of 4 — while staying green. That is the one way a ratchet fails completely: it would have let the count climb back to four without a word. It now discovers the generators by what they are rather than by a list somebody has to remember to update, which is the same lesson as gate 36 in v0.15.0 not seeing the two gates that live in JavaScript. 239 pages, every link resolving, all five gates green, 18 pages opened in a browser with no 404. admin/versions/v0.20.0.md — this note as markdown, which is what it is. |
| v0.19.0 | 2026-09-15 | The register was the end of the road, and it should have been a junction. Its own subtitle said a claim on this site walks back to here — true, and true in one direction only. You could land on /registo/#2026-09-14/entregas/…/src-pol-04, read the hash, confirm the bytes, and then have nowhere to go. The evidence is the foundation of everything this publication says, and from the foundation you could not reach the building. Every frozen file now carries what stands on it. A new column, and four kinds of thing in it: - the research delivery that named it, linked to that source's own row on the delivery page; - the articles whose assenta_em lists it, linked to the article; - the entities whose name was found in those bytes, with the number of occurrences, each linked to its entity page; - and the graph node grounded on it. So the walk from src-pol-04 now runs: the government's own release → the ChatGPT delivery that proposed it → the published article about the €200M for AI Gigafactories → the four entities read out of those bytes, Saúde 9 times, Educação 4, Jurídico 3, Governo de Portugal 2. And the article's own [[fonte:…]] mark already linked back to the register row, so the loop closes in both directions. Nothing in that column is written by hand. It is derived from the same files that build the pages — dados/historias.json, dados/entregas.json, dados/entidades.json, dados/grafo.json — so a link cannot disagree with the page it points at, and no new state has to be kept in sync. One bug, caught by building it. The first version read the delivery out of each source's congelada path. That path names whichever capture the delivery was last frozen under, and drifts: a delivery re-frozen on 15 September had a path saying 2026-09-15 while its register id still said 2026-09-14, and the link to the delivery silently vanished — the other three kinds still rendered, so the row looked complete. The delivery is now read out of the register id itself, which is <capture>/entregas/<delivery-id>/<source-id> and is the stable thing. A row that is partly right looks exactly like a row that is right. admin/versions/v0.19.0.md — this note as markdown, which is what it is. |
| v0.18.0 | 2026-09-15 | A second delivery arrives, and this time it validates. ChatGPT's first research delivery failed the published schema on 45 counts — every one of them a closed list it could not read, because the brief sent it to an address that does not exist. The address was corrected in v0.17.0. This delivery, run against the corrected brief, validated on the first attempt, re-sent none of the eight pages the newsroom already holds, and carried 3 of its 4 claims through the byte check. The delivery page becomes navigable. It was already dense — four stat tiles, the assistant's own notes, a source table with hashes and readability, an item per lead with every claim under it, and 41 queries at the end — and the only way to reach the item you cared about was to scroll. It now opens with a contents table: one row per item, its kind, how many of its excerpts were found in the frozen bytes, and the editor's decision, each row jumping to the item. Alongside it, chips to the sources, the queries and the decisions, and a strip linking the sibling deliveries — a part is one of eight, and a reader landing on one should not have to go back to the index to reach another. The frozen copies stay listed, hashed and unlinked: they are evidence, not pages of this site. The assistant found a defect in the brief, and it was mine. v0.17.0 told it to omit delivery.vault until the vault exists. The schema lists vault in delivery.required and types it ["object", "null"] — omitting it fails. The delivery said so in its own notes, used null, and validated. The brief now says to send "vault": null, and says why it changed. The freeze date was being read from the wrong place, twice. build/entregas.py --date defaults to today and re-checks each excerpt against fontes/congeladas/<date>/entregas/; point it at the wrong day and every source reads as unreadable with the build still green. v0.17.0 anchored it to the register's newest capture. That was wrong in two ways at once: the register is written by build/extract.py, which build/tudo.py skips unless --fetch was asked for, so it can be days stale; and the value was read at import, before that step could have run anyway. A freshly frozen delivery was checked against the previous day's directory, found nothing, and reported 0 of 4 sources readable — green build, silent loss. It now reads the frozen directories themselves, at the moment the step runs. Ground truth is where the bytes are. What is still not readable. The EUR-Lex PDF of Regulation (EU) 2026/1744 answered HTTP 202 with no body, so the claim about Article 113 and the date of 2 December 2027 could not be checked against anything. And the Série I PDF of the RCM that approves the national AI agenda is still missing: the assistant spent fifteen of its forty-one queries hunting it and did not construct the address by guesswork, which is the correct behaviour and is worth recording as such. admin/versions/v0.18.0.md — this note as markdown, which is what it is. |
| v0.17.0 | 2026-09-15 | The first five articles are published, and the front page stops being about itself. Until now the three blocks a reader met first were the readability of the national register, an event that names its stages two ways, and a speaker list with one capture — three stories about this newsroom's own plumbing, honest and the only things the site could claim before it had published anything, and the wrong thing to put in front of somebody who came to read about AI in Portugal. Those three are gone, along with an empty container for the national AI agenda's legal instrument. In their place: the AI Act designation that is nowhere on the government's own AI page, the Gigafactories resolution and its €200M, €25M for AI in public administration, the Data Centres Plan read whole from the gazette's PDF, and the agenda with 32 initiatives whose founding act still will not open. The rule held at every step. The editor of record decided ChatGPT's first delivery item by item: five approved, none rejected, three left undecided on purpose. A claim reached prose only when the excerpt was re-found in the frozen bytes AND the editor approved it; the claims that were not, were not written, and each article says which and why. The three undecided items all rest on a page that returns 2 346 bytes and 22 characters of visible text — rejecting them would have said the newsroom does not want them, and it wants them more than anything else in the delivery. Issue 006 unblocks all three with one PDF. The research brief pointed at a schema nobody could fetch. It published the contract at newsroom.sgit.ai/briefs/pt-newsroom-pack/… when this site is pt.newsroom.sgit.ai serving briefs/pack/ — wrong host and wrong directory, in both briefs and in the schema's own $id. ChatGPT said so in its own delivery notes and then failed validation on twelve counts, every one a closed list it could not read. The address is fixed, the closed lists are spelled out in the brief's prose as well as in the schema, and the brief now lists the eight pages already frozen so a second delivery does not re-fetch them. Counting which page lost the most claims produced the most useful rule. Nine of the ten unusable claims rested on one /dr/detalhe/ address, which renders by script. The same delivery's files.diariodarepublica.pt PDF froze 228 725 bytes carrying 30 808 characters, and both claims on it confirmed. Follow the record page to the Série I PDF and cite the PDF. build/entregas.py was never in the pipeline, and putting it there found something worse. The editor's decisions live in redacao/revisoes/ and nothing in build/tudo.py read them: an approval written by a human reached no page, and gate 13 went on believing nothing had been approved. It runs now, anchored to the register's newest capture rather than to the clock. And that is how this turned up: the same frozen PDF, with the same SHA-256 re-verified by gate 1, read 30 808 characters on 14 September and 0 on 15 September. The bytes had not changed; the machine had. build/pdf.py does AES by importing pycryptodome, the package was absent, _aes_cbc returned None, Cifra.ok stayed True, and the reader concluded the diploma was «provavelmente um PDF digitalizado» — a false statement about somebody else's document, made with the build green, while both claims resting on it turned silently to fonte_inacessivel. The AES-256 path already names the missing package; the AESV2 path says nothing. requirements.txt now names both dependencies with the reason for each and CI installs them before the gates; the fix in pdf.py is behind the deny list and is the editor's, as issue 005. Evidence that depends on which machine reads it is not evidence.
Two duplicate renderers died. Publishing exercised a path that predated the dated folders: every published article was rendered a second time at artigos/<slug>.html from conteudo/<slug>.md, and the front-page lead linked to that dead address. Nothing had caught it because nothing had ever been published. Content exists once. The archive is the complete list; the front page is a choice. /artigos/ groups everything published by the day it was published — a different date from the one in the path, which is the day the bytes were frozen, and both are shown. The chips under the lead headline now describe the lead's own sources, named by publisher and byte count rather than by the id the register carries. This release was built as v0.10.1 to v0.10.3 while dev moved on — to v0.15.0, then to v0.16.0 — in another session. The work was merged forward and renumbered once, at the end — which is what docs/guidance/concurrent-sessions.md says to do, and the three conflicting source files were build/build.py, admin/versions.json and version.txt; every other conflict was a generated file and was resolved by rebuilding. admin/versions/v0.17.0.md — this note as markdown, which is what it is. |
| v0.16.0 | 2026-09-15 | The back office stops being a report and becomes a console. The design review's second part arrived as vault e54hfntq v0.5.0 and is about /backoffice/. Its one-sentence finding is that the console "is a report about the newsroom, not a console for operating it" — 7,058px of state across nine pages, with zero buttons, inputs, selects or textareas on eight of them. You could read what happened. You could not do anything. This release implements it. The disposition of all twelve §5 items, with what blocks the four that are not done, is at /newsroom/design.html. THE QUESTION THE CONSOLE EXISTS TO ANSWER. When the editor opens the back office he is asking one thing: what is blocked on me? The old answer was a single cell in row 6 of the first table — waiting on a human — rendered in the base chip style, so it looked exactly like the five running chips above it. Meanwhile the mail page knew he had an unread message, the board knew an issue was chipped waiting on dinis.humano, and the pipeline knew stage 6 was stopped. Three pages each held a piece of the answer and the index showed none of it. pt-queue is now the first thing on the console and the only filled rank on it, and backoffice.fila() derives its items from all four sources: the editor's own board lane, cards on another agent's lane whose bloqueado_por is the editor, paper issues the formula assigns to the editor, and mail sitting in their inbox. 13 items, against the one the console used to show. There is no count attribute anywhere — the rail badge and the queue's own heading are both len() of the same list, so they cannot disagree. A number typed beside a list it claims to describe is the failure this whole publication exists to report. FOUR RANKS INSTEAD OF 159 LABELS. The review counted 159 distinct chip labels across the back office — 158 on the board alone — and ten different ways of saying "a human is blocking this", the most important of which was the least visible of the set. assets/console.css defines one scale: rank 1 a human must act (filled, and the only filled rank), rank 2 an agent is blocked (outlined amber), rank 3 running (outlined neutral), rank 4 done (a tick and no box at all). Rank 4 losing its box is most of how a page stops having 158 chips on it. The board is at 25 ranks, and what was in most of those boxes — a section name, a priority, an effort estimate, an issue number, an agent id — is metadata now, in mono beside the thing it describes. Two mappings were backwards and were corrected at the generator rather than in the mapping: the design review's own parcial was rendering filled red, which under this scale means a human must act, and editor — the one disposition that does mean exactly that — was rendering as the quiet neutral outline. THE STRIP DOES NOT MOVE, AND NOW IT CANNOT. The proposal replaces the whole back-office chrome, which would have moved the top-level menu on crossing over from the paper — the thing the editor asked in v0.11.0 that it stop doing, and which the first fix for a later layout bug quietly broke again. Both times it was found by looking at a screenshot. So site.css is still loaded in the back office, first, for the operator strip and for nothing else: the strip keeps the paper's rules and the paper's ground, console.css loads second and wins everything below it, and gate 36 opens both pages in a browser, reads the strip's box in each, and fails on a difference of more than a pixel. It reported y 8 → 0 the first time it ran, which is exactly the class of regression it exists to catch. The strip measures x=56 y=8 1168×33 on the paper and in the console. One cost came with that decision and is paid in full rather than hidden: site.css's .nav rule lands on the rail's items, which carry class="nav" too, and brought justify-content: center with it — the rail rendered centred, every label at a different indent, which is how a navigation column stops reading as a column. console.css answers it with a closed reset of exactly the properties site.css's .nav sets, so a leak is impossible rather than unlikely. THE PREAMBLE, SAID ONCE. The same 70 words opened 9 of 9 pages — roughly 300px of identical prose above every screen, which trains the operator to scroll past the top of every page, exactly where the urgent things live. It also broke this project's own rule that content exists once. It is now on guidance.html and nowhere else. That is not the rule being softened: gate 19 requires every back-office page to say it is not the publication, because this whole area is in English by the editor's decision and the condition of that exception is that a reader who lands here can see it. What every page carries instead is one sentence, in the rail rather than above the content, so it is permanently visible without ever being in the way. The gate is unchanged and still finds its string. STRUCTURE, WHICH THE BACK OFFICE HAD NONE OF. Zero <h2> in the entire back office, and five of nine pages with no heading at all — section titles were .sect divs, which are styling and not structure, so nothing could outline these pages and Ctrl-F was the only way in. All 34 of them are real <h2> now. Every page has exactly one <h1> and it names the place: the console's used to be a sentence in the publication's voice — "What the newsroom has done, what it is waiting on, and who is allowed to do what" — which describes a page rather than labelling where you are standing. What still wears .sect is a field label inside a card, "Refuses", "Writes in", because promoting sixty of those would have made every outline useless a different way. The rail replaces eleven flat links with three groups, marks the current page with aria-current="page" rather than weight alone, and carries a count on the three items that can be behind — never a zero, because a count that is always there has stopped being a signal. REAL CONTROLS, AND HONESTY ABOUT WHAT THEY DO. The bridges page's controls were the only real ones in the back office and were dressed as status chips, in the same box as "secret" and "in transit"; they are buttons and fields now, primary for keeping a credential and danger for forgetting one. The queue's action names the file it would write in its title, so an operator who does not trust a button can do the same thing by hand. But a button cannot write a file on a static site — there is no server to write it — so each one goes to the place the write happens: the document browser for a file, the mail page for a thread, the bridges page for a message to the newsroom. Nothing claims to have written anything. Making them act is the append lane's job, the lane is built, and it has no credentials: that is §5.10, and it is the editor's. THE MAIL MODEL, SHOWN RATHER THAN EXPLAINED. The review called the mail protocol the best thing in the back office and its presentation the weakest: a paragraph and a four-row table for something that is a path a file walks. It is the path now — saida/ → expedicao/ → entrada/ → tratado/, each folder carrying the rank its state means — and it needs no paragraph. The protocol's own table is still published, folded into a <details>, because the detail is real and is not what the page is for. The mailbox table gained a state column and the editor's row is marked on the row: a state marked only inside a cell is one you have to hunt for by reading every row. THE LEGACY LAYER, AND THE RATCHET ON IT. Nine generated pages emit the paper's class names, and rewriting every generator in the release that adopted the console would have been a much larger diff than the review. So console.css maps those names onto the console's own — lossily, and in the direction the review asks for. It is a bridge with a declared end state, and gate 39 is what makes that more than a promise: it counts the generators still emitting the paper's classes and holds the count to a ratchet, which may fall and may not rise. The first version of that gate counted usages in the built HTML and went red on the very next commit, because that commit added twelve items to the design-review page. The number had gone up for the right reason and the gate could not tell. A metric that rises when the newsroom publishes more is not measuring the bridge. Two defects in the adopted files, found by the gates. pt-queue.css hardcoded #fff and #000, which gate 33b rejected — the same finding the review itself had made about the five shipped components, in the component the review shipped. pt-queue.js reported a read failure by telling the operator to look at "the list below" while its own stylesheet hid that list; a read failure now un-hides it. And a third, introduced here and caught at 390px: copying white-space: nowrap from the rank scale onto the legacy .chip, which is whatever a generator put in it — including a whole sentence — pushed the console 75px past the viewport. Not done, and why. Four §5 items are open and each says what blocks it on the design page. The filenames are not renamed to console.html / mail.html / board.html: those addresses are published in llms.txt and the sitemap, this site's own argument is that a link resolves, and whether ten redirect pages are worth the consistency is the editor's call. The vault channel's reply box is not built, because it needs three credentials this repository may not hold. The Portuguese left in the running prose is quoted — an agent's mission, a card's own reading of its work, the formula's porque column — and paraphrasing a record on the way to a screen is how a console starts to drift from the files it reports on. And the review's ~2,600px page height would mean deciding which of the roster, the articles, the sections, the deliveries and the run records the console stops showing, which is an editorial decision and not a design one. admin/versions/v0.16.0.md — this note as markdown, which is what it is. |
| v0.15.0 | 2026-09-15 | Three Claude Code sessions work on this repository at once, and until now nothing here knew that. A gate reads the working tree, and the working tree is one session's opinion. Everything that has gone wrong between sessions went wrong in the gap between my branch is green and my push is accepted. This release closes as much of that gap as a repository can, and says plainly which part it cannot close. Gate 36 — no two gates share a number. Two sessions added gates the same afternoon and both started at 27. The build was green on each branch and stayed green after the merge, because a gate number is a comment: nothing reads it, so nothing checked it, and for a while this site had two gate 27s saying different things. The scan is deliberately tolerant, since three gate files write their numbers three different ways — and it caught its own author first: the block adding gates 36 to 38 did not match any pattern, so the gate reported «next free: 36» while sitting under a heading that reads 36-38. Gate 37 — a class a component puts on the document has a rule in the stylesheet. This one is the interesting failure. <pt-chat> sets pt-chat-aberto on <html> and assets/site.css turns it into a column. The v0.13.0 merge brought a wholesale rewrite of that stylesheet; git auto-merged it with no conflict at all, and the four rules went with it. Nothing failed — the component still loaded, still reached pronto, still had a shadow root full of text — and the panel simply lay across the article again, the same symptom as the hidden bug two releases earlier from an entirely unrelated cause. A coupling that crosses a file boundary and is checked by nothing will eventually be deleted in silence. Gate 38 — the release history is a set, and the version is its newest member. Two sessions took v0.13.0 within the hour. The site gate checks the table has a row for version.txt; it never checked there was only one, nor that the number was ahead of every other, nor that the note file named actually exists — and build/versoes.py skips a missing note silently, which turns a mistyped path into a release shipping with an empty row. build/before_push.py asks the one question a gate cannot: what is on the release branch that is not in my hand? It fetches, reports and changes nothing — the released version and whether yours is free, the highest gate number on each side, and the list of source files the other session rewrote wholesale. Then take your version number, which should be the last thing you do and not the first.
That last list was wrong when it was first written, and the wrong version is the instructive one. It listed files both sides had changed since the merge base — the obvious heuristic. Checked against the merge that actually destroyed work, that list did not contain assets/site.css at all: the rules had landed two releases earlier, so they were already in the merge base and only their side had touched the file since. The heuristic could not have caught the thing it was named after. What catches it is churn — their rewrite changed 897 lines of a 411-line file, where the next busiest source file on the same range changed 31% of itself. And the guidance page that holds it together, at [docs/guidance/concurrent-sessions.md]( ../docs/guidance/concurrent-sessions.md) and in the back office: what has actually collided, what each thing cost, which gate catches it now, and the ordering that avoids most of it — claim the work before you build it, take the version number last, resolve a generated file by rebuilding and never by hand, and when the other session rewrote a file wholesale, read your own lines in it afterwards. A clean merge is not evidence that your work survived. Every one of the three gates was broken on purpose before it was trusted: a second file numbered from 34, the chat's column rules deleted again, a duplicated release entry, a note file that does not exist, and a version number already taken. All five go red; all five were restored. admin/versions/v0.15.0.md — this note as markdown, which is what it is. |
| v0.14.0 | 2026-09-15 | The chat's tools were fetching from the wrong place on every page but the front one. Reported by the editor with a screenshot: asking «what is the article I'm reading now?» came back with listar_artigos falhou: articles.json respondeu 404. The engine built the path to /api/v1/ by counting the segments of location.pathname, and it was wrong twice over in a way that hid itself — the count was short by one for a directory URL (/artigos/2026/09/14/<slug>/ needs five ../ and got four), and the result was then joined to a prefix that began with a slash, so the whole address collapsed to /artigos//api/v1/. On the front page raiz() returns nothing and the absolute prefix saved it, which is why it looked like it worked. Counting was the mistake, not the arithmetic. assets/conversa.js sits at <root>/assets/conversa.js, so one step up from its own URL is the site root — at any depth, under any prefix, with no rule to keep in step with the page tree. It is the mechanism SgComponent already uses to find its own markup (static jsUrl = import.meta.url), and the reason a component can be moved without being told where it went. A 404 now also names the address actually requested, not just the path, because the old message sent you looking at a file that was there all along. The second half of the same report: an id that was quietly repaired into a wrong one. The model called ler_artigo with 2026/09/14/dois-nomes-para-os-mesmos-palcos; the engine stripped the characters an id may not have and fetched 20260914dois-nomes-para-os-mesmos-palcos, a path nobody wrote. Silently repairing an argument turns a wrong question into a wrong answer — on this site the one failure mode that does not look broken. It now refuses, and says what an id is, which tool hands them out, and gives a real example. Each {id} tool carries its own example in the schema, and ler_artigo states outright that the folder is dated but the id is not. A gate that asks questions. The chat's tools are only exercised when somebody asks something, and no gate asks anything — so the browser check now calls every listing tool for real, from the deepest page on this site and from the shallowest, and reads one article by an id the listing itself handed out. Both paths are needed: the article page is where the arithmetic broke, and the front page is where the old code accidentally worked, so a deep-only check would have passed a half-fix. Gate 34's scope stopped one directory short of the code that mattered. It covered assets/components/ and missed the four scripts directly above them — the bridge, the observer, the graph viewer and the chat engine, about 240 comment lines, two of them about what leaves the reader's browser. All four are now in English and in scope, vendored third-party code excepted because rewriting it to our taste would break the reason it is vendored at all. And the gates themselves now fail in English. Fifty-eight failure messages in build/gates_artigos.py were Portuguese while the summary line above them was English. A gate message in a terminal is read by whoever operates this newsroom, so it follows the same rule as the rest of the code; values quoted out of the files — a state, a section, a name — stay exactly as the file has them, because those are data. Left for the migration release, recorded here rather than fixed quietly: the article state superseded is an English id in an otherwise Portuguese vocabulary. The label a reader sees is «Substituído»; only the id is out of place, and ids are what that release is about. Also: admin/versions.json carried a hand-maintained contagem that drifted the first time two sessions each added a note. It is now counted from the list, and rewritten when it disagrees. And a merge lost something quietly, which is worth recording because nothing failed. The design review's stylesheet arrived as a wholesale rewrite of assets/site.css; git auto-merged it without a conflict, and the four rules that give <pt-chat> its column went with it. The panel still opened and simply lay across the article again — the same symptom as the [hidden] bug two releases ago, from an entirely different cause. They are back, with a comment saying so. admin/versions/v0.14.0.md — this note as markdown, which is what it is. |
| v0.13.0 | 2026-09-15 | The design review's proposal, adopted. The editor asked for the vault's proposed design to be implemented, and this is it: assets/site.css is the proposal's stylesheet, and the generators now emit the vocabulary it introduced. One value is corrected, and the correction is the interesting part. THE ONE NUMBER THAT CHANGED, AND WHY. The proposal sets --measure: 34em with the comment "~66 chars at 18px" and --measure-sm: 38em at "~68 chars". Both are wrong, and the second kind of wrong matters: it derives them from an average glyph width of 0.48em, the usual figure for a screen serif, where Newsreader — measured in Chromium on a canvas at the element's own computed font with real Portuguese text — averages 0.405em. About 18% narrower. Two consequences. The problem was worse than the review reported: the 758px column held ≈104 characters, not 88, and the 828px entity column ≈114, not 96. And 34em is 612px, which measures 84 characters — above the 75 the review's own §4.4 sets as the ceiling, and wider than the 29em live since v0.9.0. Adopting the proposal literally would have regressed the thing it exists to fix. The site stays at 29em, measured at 71 characters. The two tokens are the same value because an em measure is size-independent: the font size cancels in width ÷ (size × glyph), so 34em and 38em could not have given 66 and 68 in the first place. THE BANDS, which are the structural half. One ground for a whole page is why the sections ran together: hairlines alone cannot separate blocks identical in tone and density. A newspaper varies the ground as well as the rule, and a reader navigates by that before reading a word. .band wraps a section and carries its own ground, with a .folha inside it so it can be full-bleed — which meant pagina() had to learn corpo_em_bandas, because a page that bands itself opens and closes its own sheets. The front page uses three grounds and not six: base for the lead, recessed for «Em preparação», base for the rest. A page that alternates every block is as flat as one that alternates none. The section opener, at three weights. .sect alone is a label, not a door, and using it for both is the other half of why the page read as one field. .opener--major gets a 3px ink rule, a number and a title; .opener gets 1px; .sect stays the label for a block inside a section. This is deliberately not a blanket replacement for the 104 .rule + .sect pairs across the generators — the stylesheet's own note says to reach for the heaviest weight three or four times at most, and a page where everything is a door has no doors. The front page uses it once. The provenance, demoted without being removed. The chips sat directly under the lead headline, where a newspaper puts the dek — three bordered 11px mono runs of character counts, turning a well-written lead into a debug panel. Now a sentence in the paper's own voice leads — "Assenta em 3 páginas congeladas hoje · 2 não devolveram texto" — and the chips open behind a <details>/<summary> disclosure. No JavaScript: it has to work with scripting off, and half this site's readers are machines that take the chips from the markup regardless. The sentence is derived from the same dict the chips are built from, so the two cannot disagree. The stat strip, replacing a stack of label-plus-sentence rows where the counts — the thing an eye goes looking for — were buried mid-prose. And it immediately caught me repeating myself: the strip carried Oradores, Organizações, Fontes and Grafo, and the rows underneath said the same numbers again in sentences. That is «content exists once» broken by the session that keeps quoting it, and it made the rail run 406px longer than the column beside it. The strip keeps the numbers; the rows keep only what a number cannot say. The column hole, and where it went. The review measured ~270px of blank paper at the foot of the left column while the rail ran on. «Nesta edição» is a summary of the whole edition rather than a third secondary story, so it moved under the lead: that filled the hole and took the same height off the rail. The imbalance is now 341px the other way — the lead column runs longer than the rail, which is what a broadsheet front page looks like. .g12 > .col { align-self: start } means the short column simply ends and its border stops with it, instead of a rule running down beside nothing. Two more stacking bugs found by looking at the rendered page, not by reading the rules. The masthead band closed with 48px of its own and the lead band opened with another 48, stacking into ~120px of blank paper between the nav's double rule and the lead headline — on a front page, where the lead is supposed to be the first thing under the nav. The nav's 3px double rule is already the divider, so the first band's bottom padding goes to zero. And the lead grid still carried padding:34px 0 30px inline, a second helping of the room the band now provides — which is exactly the inline padding the review wants the scale to replace. Gap: 120px → 86px. Measured after, in a browser: 71 characters per line, 5 bands, 3 full-ink .rule divisions (the stylesheet's target is ≤3), 1 major opener, and the mobile chrome above the masthead down from ~130px to 88px — the review's target is ≤48px and its own proposal reached 96px, so this is past the proposal and still short of the target, which needs the dateline abbreviated on small screens and is a generator change for another release. Gate 29 learned to follow a token. The stylesheet names the measure — .std { max-width: var(--measure) } — which is right: the value lives in one place. But the gate looked for a literal em and so reported that .std had no measure at all while the measure was correct. A gate that cannot follow one level of indirection ends up forbidding the tidier code, which is the wrong way round. What was NOT adopted, and it is the one thing the editor still owns. The proposal's front page de-duplicates the card grid, and this one does not. §4.5 decision 5 asks the editor to confirm that a front page showing three stories instead of six — more honest, visibly emptier — is acceptable, and says in as many words: "Agents: do not pick these yourself." The filter is written and switched off. The other four decisions are likewise still open, on the editor's board. admin/versions/v0.13.0.md — this note as markdown, which is what it is. |
| v0.12.0 | 2026-09-15 | The chrome stops moving when you cross between the paper and the back office, the spend gets a page of its own, and a layout bug this session shipped two releases ago is fixed. Three things the editor asked for, and one he found by looking. The menu moved, and it moved a lot. The paper had a dateline, a countdown and a run of utility links, then a centred masthead and a section nav. The back office had a single row mixing its own identity, its own eleven links and the version — different order, different place. Clicking «Back office» rearranged every item in the chrome at once, which reads as arriving at a different site rather than at the back of the same one. The utility run is now generated once, in build/paginas.py, and both chromes emit it in the same slot with the same items in the same order. Measured in a browser: on both pages it sits 18px from the top and 56px from the right, 18px tall. Exactly one item differs, and it is the one that has to — on the paper it points into the back office, in the back office it points back out at the paper. The console's own links become a nav below it, wearing the paper's .nav with a single hairline instead of the broadsheet's double rule, because the back office is not the publication and should not claim the publication's furniture. It also now marks the page you are on, which the paper's nav always did and the back office never did. A layout bug this session shipped in v0.9.0, and the editor caught it. Capping .std at 29em is right for the newspaper, where a paragraph sits inside a grid column about 758px wide. The back office has no grid: it is one column of prose in a 1328px sheet, so the same cap put the text in a 522px ribbon with 800px of empty paper beside it while every hairline went on spanning the full width. Text at a third of the page with rules across all of it does not read as a narrow measure — it reads as broken, and it was. The fix is the container, not the paragraph: the console's content column is 58em, so a rule is 1.78× the prose it divides instead of 2.5×. The first attempt at this fix put the width on .folha and quietly undid the chrome work above — the utility run ended up 312px from the window's right edge against 56px on the paper. Same slot relative to its own sheet, different place on screen. The narrowing is on the content and deliberately not on the sheet, and the stylesheet says so where somebody would be tempted to move it back. The wallet stops being a popup. The editor asked for the budget spend on a page of its own, and the reason is not taste: the ledger is the interesting part of the demonstration, and a ledger worth reading is worth an address. A panel that covers the masthead cannot be linked to, cannot be read on a phone without covering what you were reading, and closes itself on the first click elsewhere. pt-wallet gains two modes on one template — a badge that is now a link in the chrome, and the ledger itself at /carteira/ with a stat strip, the actions and the last pages read. There is no second copy of the ledger: two copies would diverge the day somebody changed the price in one of them. The badge still debits the page it is on, because the debit is the demonstration and has to happen wherever the reader is. The wallet page carries both instances, and that is not a double debit — _charge() records the page in sessionStorage before returning, so the second one finds it already seen. The guard was written for the back button and happens to cover this; the comment says so rather than relying on it by accident. This also settles half of the design review's decision 4: the wallet is out of the reader's header as a panel, and whether the badge itself belongs there at all is still the editor's call. The palette did not reach inside the components, and the design review found it. The five shipped components hardcoded #0f766e, #f7f4ec and most of the rest of the palette in their own CSS — so the v0.9.0 contrast fixes landed everywhere except inside them, which is the half of the site a reader spends longest looking at. Custom properties do cross the shadow boundary, because they inherit: a component reading var(--acento) picks up a palette change for free. It just needed a token to inherit, and there were none. 156 hardcoded colours across seven component stylesheets are now tokens, including eight near-misses — a paper that was not the paper, a hairline that was not the hairline, an ink that was not the ink — which is exactly the drift tokens exist to prevent. The one genuinely different colour, the purple that marks a JSON key, becomes --sintaxe-chave in :root: not because it was failing (7.93:1, comfortably above AA) but because a colour that is not in :root cannot be measured by the gate, and measured is the property that matters. Gate 33b now fails the build on a hardcoded colour in component CSS, with a var(--token, #fallback) still allowed — the token wins whenever it is defined, which here is always. Injection-tested: it catches an accent put back by hand. Renumbered from v0.11.0, and the gate earned its keep on the way. The other session shipped its own v0.11.0 while this one was building — the third time today two sessions have reached for the same number — so this release moved up, because the one that has not shipped is the one that moves. Merging their work brought in a brand-new pt-chat component, and gate 33b failed the build on it immediately: it hardcoded #fff for the tab label and dot. There is no pure white in this palette; --papel is the house colour for light on ink, which is what .escuro already uses over --tinta, and on the accent it gives the same 6.83:1 the accent gives on paper. A gate written for one release catching the next release's component, on its first run, is the whole argument for writing it as a gate instead of a note. And this release failed to deploy the first time, with all five gates green. Worth writing down, because the failure mode is the nastiest kind. validate passed, the five gates were green, the push to dev succeeded — and tag-release failed with "version.txt says v0.12.0 but the newest release commit in this history is v0.11.0". CI does not read version.txt alone: it reads the newest commit subject matching ^site vX.Y.Z:, because the subject is what names the commit to tag. This work went in under a merge subject — "integra o dev (v0.11.0) e renumera para v0.12.0" — which carries the number but not in the form CI parses. So nothing was tagged, deploy was skipped, and the live site stayed on the previous release while every local check said OK. A green build that does not deploy is the worst kind of green, and rewriting the subject was not an option: this repository does not force-push and does not rewrite history. build/tudo.py now closes by running the same comparison CI runs and saying, in as many words, what the release commit's subject has to be. It is a warning and not a gate, deliberately: on a feature branch version.txt is legitimately ahead of the newest release subject for as long as the work is unfinished, and a gate that failed every intermediate build would teach whoever hit it to skip gates — which costs more than it saves.
admin/versions/v0.12.0.md — this note as markdown, which is what it is. |
| v0.11.0 | 2026-09-15 | The chat stops being a card at the bottom of the page and becomes a column beside it. Modelled on VoiceDebrief: a vertical tab on the right edge, a panel that opens from it, and a drag handle to set the width. The distinction that mattered here is column, not overlay. An overlay covers the page you were reading, and on this site the page you were reading is the thing you are asking about — «em que fontes assenta isto» is not a question you can answer while the answer is behind the question. So <html> takes a padding-right equal to the panel and the article reflows beside it. The width, the open state, the materials you send, the model and your key all live in localStorage, so the panel opens the way you left it. The engine was already here and was kept. assets/conversa.js had the whole apparatus — the level-0 comparator that runs in the browser with no key, the 18 GET tools, the OpenRouter call — and it was checked for before anything was written, because this repository has already produced two answers to one ask once this week. Only the presentation was replaced. What changed inside the engine is that it now emits each tool call as it happens and asks OpenRouter for usage, so the panel can show which file was opened, with what argument, how many bytes came back, and what the round cost — inline, where it happened, rather than behind a spinner. On a publication whose whole argument is that a claim walks back to bytes, hiding which bytes were read would be the wrong thing to hide. Every tool is visible before you use one, and every one says LER. Not as a safety claim but as a consequence: the API of this site is files, so there is no write to offer. The panel also lists what travels with your question, as checkboxes, because a reader should never have to guess what left their browser. The panel shipped broken, and this is how it was caught. .painel { display: flex } in the component's own stylesheet silently beats the browser's [hidden] { display: none } — one class selector against one attribute selector — so the panel that was supposed to be closed was lying across the article on all 227 reader pages. It passed every gate: the component reached pronto, nothing threw, nothing overflowed, nothing 404'd. It was found by taking a screenshot and looking at it. Gate: the browser check now reads the computed style of every element carrying hidden inside a component's shadow root, for every component, and fails when one is on screen anyway. And the browser gate stopped sleeping. It waited 400 ms after networkidle and then asserted — which on /grafo/, where Cytoscape runs its layout on the main thread, found pt-chat with an empty shadow root and reported a broken component that was fine at three seconds. A fixed sleep measures the machine the gate runs on. It now waits for the data-estado the base class sets, up to ten seconds, and a timeout is still a failure — a component that needs longer than that is one the reader sees as an empty box. Three deliberate injections confirmed the gate fails: a selector that is not in the template, a renamed .tpl, and the [hidden] rule taken back out. Sixteen pages, five gates, green. admin/versions/v0.11.0.md — this note as markdown, which is what it is. |
| v0.10.0 | 2026-09-15 | The language rule gets a test, a gate, and an apology. One question decides every naming question here: would a visitor read this string on the site? Yes means European Portuguese; no means English. CLAUDE.md has said "code and comments are in English" since the first commit, and seventeen Python files ignored it — including most of the ones written this week — because the code already there was Portuguese. An existing convention is not the rule. Roughly 880 comment lines across 22 files are now English, and gate 34 fails the build on the next one. The gate was wrong twice before it was right, and both bugs are worth knowing. Its first version matched triple-quoted blocks with a regular expression and flagged eleven page templates in build/build.py — blocks of HTML holding the Portuguese a reader sees. It was telling the site to stop being Portuguese. Its second read ## markdown headings inside the llms.txt string as comments. Both are the same mistake: guessing at syntax a parser already knows. It now uses ast and tokenize, and skips «guillemets», because an English sentence naming a Portuguese section is still an English sentence. Three files are exempt, by name, and the exemption is the editor's to clear. build/gates.py, build/entregas.py and build/pdf.py are in the deny list of .claude/settings.json, so no agent here can edit them — and a gate that fails the build over a file the agent is forbidden to touch is not a gate, it is a deadlock. They are named in the gate, counted in its summary, and listed here rather than quietly skipped. Every agent gets a ROLE.md and a MANDATE.md — rendered, not written. And the story of how is the useful part: two sessions answered this same ask on the same afternoon. One wrote five mandate files by hand; the other built dados/agentes.json, which already held every field those files would state. Two answers to one ask is two copies, and two copies diverge the day somebody edits one. The register won — it shipped first and is wired into the API, the console and the mail — and the hand-written files were deleted. build/mandatos.py now renders agents/<id>/ROLE.md and MANDATE.md from it, marked DERIVED FILE, with English scaffolding and the register's values quoted verbatim in Portuguese. Gate 35 fails a run naming an unregistered agent, a registered agent with no mandate file, a mandate not marked as derived, and — the one worth having — a mandate whose domain, mission or central claim has drifted from the register. A mandate that no longer matches reads as authoritative while being wrong. Guidance, at docs/guidance/ and in the back office. Four pages, in the order they should be read: the language rule, the nine principles each naming the gate that enforces it, the before-you-change checklist, and what not to build. Modelled on sgit.ai/docs/guidance, and on the lesson from coding.sgit.ai that measured its own estate and found a guard that had never matched anything: a rule without a gate is decoration, so each principle names its gate or says it has none. Evidence from a sibling publication, kept as its evidence. Fourteen frozen files from newsroom.sgit.ai — two Startup Summit captures from 8 and 13 September, and three press pages — verified against their manifest and stored under fontes/transferidas/, never in our own register. They were frozen by another publication's fetcher, at its times; entering them as our captures would make this site's method quietly untrue, which is the one failure mode that does not look broken. They turn a story explicitly waiting for a second capture into three captures in six days: 60 → 64 → 70 speakers. admin/versions/v0.10.0.md — this note as markdown, which is what it is. |
| v0.9.0 | 2026-09-15 | Released by @Bastidores. Translated into English here, like every other note, under the language rule adopted in v0.10.0 — the words are that release's, the language is the house's. The design review, implemented — and what it measured becomes a gate. The editor commissioned a design and legibility review, with the complaint that "the current design is hard to read, and does not look as professional and smooth as a news site or a strong blog should". The review arrived in an encrypted vault (e54hfntq, v0.1.0, against build v0.6.0) and did the unusual thing for a design review: it measured. Characters per line derived from the grid rather than eyeballed, contrasts computed from the tokens in :root, empty cells counted one by one, screenshots at 1440px and 390px. That is why this release is large and specific rather than a matter of taste. "Hard to read" had four measured causes, and they compounded each other. The text column ran 88 characters per line on the front page and 96 on an entity page, where a newspaper column sits at 45–75 — above that the eye loses the return to the next line, and the experience is exactly "I can read it but it is tiring". Leading was at 1.45, where a serif on cream paper wants 1.6–1.65. -webkit-font-smoothing: antialiased was drawing the text lighter than specified, taking weight off the strokes Newsreader needs at 15–18px. And the rules were not visible: --filete sat at 1.36:1 against the paper, when the standard asks 3:1 of a non-text element carrying meaning. This stylesheet's entire thesis is "rules, not boxes" — structure is carried by rules — and the structure was being carried by lines that, on a light screen, are not there. That is why the front page's blocks appeared to run into one another. "Does not look like a news site" had one dominant cause, and it cost ten lines. IBM Plex Mono set the dateline, every kicker, every section label, the front page's thirty chips, every table header and the entire footer. Cumulatively the page read as a build log with a good headline. A newspaper does not set its section labels in a typewriter face. The kicker and the section label move to the serif, and with them the table header and the prose of the "for an agent" block. The rule now written into the stylesheet: the mono face means one thing — a machine fact. A hash, a byte count, an identifier, a time, a path, code. The accent had seven jobs and therefore signalled none. Kicker, current section, link-on-hover, confirmed source, ok-chip, graph verb and "open the graph". It now has two: interactive, and confirmed. The kicker leaves it. And three of the four state colours failed or scraped AA — --disputa was at 4.48:1, below the line, carrying verification state at 11px. In a publication whose whole proposition is that a reader can see the verification state of every claim, that is not a contrast detail. The ramp darkened: accent at 6.83:1, warning at 6.06:1, disputed at 5.81:1. Seven gates, 27 to 33, so it is not reviewed by eye again. The review's §4.4 said the thing that stops a review becoming decoration: "Do not review by eye again. These are the numbers that changed. Each is cheap to assert in a build check." build/gates_desenho.py is that file — each gate reads a value off disk and compares it with a threshold the review wrote. Two of the seven numbers need a browser and live in the browser gate instead, which is said rather than omitted. And one number in the review was wrong, in the direction that mattered. The review used 0.48em as Newsreader's average glyph width, the usual figure for a screen serif. Measured in the browser — a canvas, the element's own computed font, real Portuguese text — it is 0.405em, about 18% narrower. That cuts both ways: the problem was worse than reported (the 758px column held ≈104 characters, not 88), and the fix the review asked for did not reach the target it set (34em is 612px, which measures 84 characters, still above 75). So the site's measure is 29em, not 34em, and the gate measures with 0.405. The way to get the number again is the same as the way it was got: measure, do not estimate. admin/versions/v0.9.0.md — this note as markdown, which is what it is. |
| v0.8.0 | 2026-09-15 | Released by @Bastidores. Translated into English here, like every other row, under the language rule this site adopted in v0.9.0 — the words are that release's, the language is the house's. The agents become a register, and the mail between them becomes a protocol. It was three paragraphs in dados/equipa.json and two loose messages in a folder. dados/agentes.json becomes the register, in the form teams.sgit.ai publishes for a ROLE.md crossed with the one sgraph.ai publishes for each agent on its team. Five roles, and the fifth — @Bastidores — writes not one line of the paper. Five others are named as not built, with the reason, because a register claiming roles nobody runs would be claiming a capability. The field that makes this a team is "not responsible for". Without it every role silently becomes the same role — which is why it is in every definition, saying whose the work actually is. Each role's central claim is written as a failure condition, so it can be contradicted rather than admired. Email-FS-lite, as published, with the difference said out loud. The mail runs on the protocol the sgraph.ai team publishes and runs: one writer per folder, dispatch as the only shared surface, one commit per cycle, immutable .eml messages that only move. A message's state is the folder it is in and not a field, because a field can disagree with the folder and a folder cannot disagree with itself. The protocol was designed for a vault, where sgit pull is the inbox notification; here it runs in a git repository, and that difference is written in redacao/correio/LEIA-ME.md rather than disguised. One reader, and a second reader caught out of date. build/equipa.py reads the folder and derives dados/correio.json; everything else reads the derived file. v0.3.1 was the deletion of a second document reader, and this cycle's integration showed why: build/mesa.py counted the mail by opening the folder, and after the migration counted zero without failing. A fault that does not fail is the worst kind, and two readers created it. There were also two lists of agents — one hand-written in build/comentarios.py — and now there is one. The bridge: the editor can talk to the newsroom from the browser. This site is static and cannot write to the repository; a vault append queue closes that gap without giving it a server, because it splits access into four capabilities that do not overlap — whoever holds the code appends and nothing else, and the response to an append is blind on purpose. It is built and credential-free: the vault id and the code are given to the browser and live in localStorage, because the validator's key-shaped-string tripwire would fail the build if one entered a file — which is the gate working rather than a rule being remembered. A finding from the games vault that changes the design. Its telemetry built events and never sent them. The cause was not the code: a vault app runs in a frame whose CSP is connect-src blob: data:, and the only way out would reopen every exit from a frame holding decrypted content. This site is not a vault app — so what the games telemetry could not do, this site can. The difference is one of surface, not of effort. "Talk to this content", at both levels, and level 0 is the default because it says why it chose. A deterministic comparator running in the browser against the index the build emitted, showing the words that matched and where. Level 1 is the reader's own key, with eighteen tools that are GETs to /api/v1/ — unusually safe to hand a model, because the API is only files and the worst a call can do is read something already public. Four new API collections: agents, mail, board and bridges. What is blocked, and is the editor's. dados/aviso.json does not mention telemetry, and while the bridge is off that is correct. The moment it takes credentials the notice is wrong by omission — and the IP address the queue's server sees has to be written down, because it is the one part the reader cannot verify. The order is the notice first, the credentials after, which is the order this site already got right once, when the notice was published before the first page that names anybody. The review of the other agent's work, and three things it changed. The browser gate he added the same night — admin/build/render.mjs, which opens every page in a real Chromium — found a real defect in this version the first time it ran over it: the bridges page overflowed at 390px, scrollWidth 413 > 390. The cause was a <select>, which takes its width from its longest option and will not shrink below it. Fixed, and it is the gate's argument made in public: none of the other three would have seen it. The gate did not cover the new pages, and the omission was exactly the exemption gate 26 forbids. render.mjs's page list is made of components, and this version's four pages use none — but they have code that runs, and the bridges page has the largest script block on the site. All four join the list: 15 pages in a browser instead of 11. And "ready" on two components did not mean what the base class says it means. The comment on pronto() promises data-estado is only set once the component "has actually finished loading whatever it loads". pt-json-viewer and pt-doc-browser started their load without awaiting it, and so became "ready" when the shell mounted — the same false health falhou() was added to remove, one level down. Both now await what they started. admin/versions/v0.8.0.md — this note as markdown, which is what it is. |
| v0.7.1 | 2026-09-15 | The skill a scheduled run follows was telling it to do impossible things. .claude/skills/newsroom-run/SKILL.md said git pull origin main and git push origin main — and this repository has no main; the release branch, which is also the default branch, is dev. And it said to run node build/validate.js, which does not exist: the validator is in admin/build/. A scheduled run would have died on the first line of the first step. And the record it told the run to write was not the one the gates read. The field names were all wrong — inicio, issues_movidas, ficheiros_por_pasta, portas — where gates 12 and 26 read quando, issues_movidos, pastas_alteradas and portoes. The run would have produced a record the department boundary cannot check, and since gate 26 it would fail outright for declaring neither a department nor a species. And the build sequence had six steps where there are now eleven. It becomes python3 build/tudo.py, which is the real order and the only one that cannot go stale without something failing. The publish step now fetches dev before choosing a version number, because another session may be working here at the same time, and two picking the same number means one has to undo its release. admin/versions/v0.7.1.md — this note as markdown, which is what it is. |
| v0.7.0 | 2026-09-15 | The fourth gate: open the pages in a real browser. Three gates read files and read HTML. Neither executes anything. A component that throws on load — a wrong path, a fetch for a renamed file, a syntax error in a module — passes all three and reaches the reader as an empty box. And an empty box looks like a design choice. admin/build/render.mjs opens every page carrying a component in a real Chromium and fails on a console error, a failed request, an empty component, or a page overflowing at 390px. And its first injection test found a hole in the gate itself. A component that catches its own error and draws a friendly message is doing right by the reader and wrong by whatever checks the page: no console error, no exception, a shadow root full of text — it looks healthy. An exception mid-_render() got through. The fix was not in the test: the base class gained falhou() and data-estado on the host, outside the shadow root, and every component now says whether it came up. Six injection tests, including one that never finishes loading. The distinction that was kept. A request the READER made and that failed does not mark the component as in error — in an API console, watching a request fail is half the point. falhou() means "this component did not come up", and widening it would erase exactly the distinction that makes it useful. A favicon, and one fewer 404 on every page. Chromium asks for /favicon.ico unprompted and no gate saw that 404, because it is nobody's link — the browser gate found it. The site's paper, ink and accent, and nothing else: a paper that has not published a single story has no logo to put there. Edge targets start looking like what they are. On an entity page the value of the page is being able to follow the sentences — and the house style only underlines on hover, which makes a link look like text. A class of its own, .no rather than .ent, because gate 23 counts .ent links — one per entity per page — and a list of sentences repeats the same node many times, deliberately. The same thing to a reader, different things to a gate. admin/versions/v0.7.0.md — this note as markdown, which is what it is. |
| v0.6.0 | 2026-09-15 | The desk becomes a room. /redacao/ opens with four benches — Research, Newsroom, Verification, the editor of record — each showing how many cards are waiting on it and how much of its work is still open. Click one and the room answers: what it does, what it refuses to do, which folders it writes in. A table shows rows; a room shows load, and load is the question you ask a newsroom at five in the afternoon. There is no way to move a card, and the absence is the design. A story's state lives in the files of its own folder, and whoever changes it is whoever has write access there. A room that let you drag a card into «publicado» would be handing out, in one click and to anyone who opened the page, the exact gesture this publication reserves for a named human. The plain table stays below, JavaScript-free: half this site's readers are machines, and a newsroom only one of them could read would be the wrong irony. A defect caught in the building of it. The first version of the board assumed one article per issue and hid an article — issue 001 commissioned two stories, and the board showed six cards and looked complete. A board that hides work is worse than no board, precisely because it looks complete. The card becomes the ARTICLE, which is the thing with files. Gate 26: "construction" is not the word you write to escape the boundary. Gate 12 exempts a run that declares no department, because the bootstrap session creates everything. This week's sessions are construction and use that exemption — so they now have to earn it: freeze no source, move no card, publish nothing, record no version on red gates. Five injection tests. An exemption any run can claim by typing the right word is not a boundary — it is a door with the name written beside it. admin/versions/v0.6.0.md — this note as markdown, which is what it is. |
| v0.5.0 | 2026-09-14 | The agents' work goes on display — and none of it was written. Every article gains a comentarios.json and a map below its provenance: who said what, when, and what was left open. 62 entries, 5 agents, 35 still open. The easy way to do this would have been to write the comments. They would read well — one from ChatGPT suggesting, one from Perplexity disagreeing — and none of it would have happened. Attributing to another provider's model a sentence it never wrote is worse than a claim with no source: it is a claim with a false source. So every entry is DERIVED from a record that already exists — the provenance timeline, the verification record, what the newsroom says it still lacks, what arrived from outside with the result of searching the frozen bytes for its excerpt, and the editor's decision or its absence — and names in its de field the file and path it came from. Gate 25 opens that file and walks that path. If it does not resolve, the build fails: the claims rule turned inward. Seven injection tests, and the last is the one worth keeping — an agent name nobody declared is an anonymous contributor to a publication whose entire argument is knowing who said what. An outside proposal attaches to an article by SECTION, and says so in full. "It arrived for the policies section, which is this article's section" — not a claim that it is about it. That would take somebody reading both, and that somebody is the editor. build/tudo.py: the whole build in one command. Eleven steps and the order matters — entidades.py must run before anything that writes a page, because its output is what turns a mention into a link. Out of order nothing breaks; the site just quietly has fewer links. The command list in CLAUDE.md is older than half these steps, and that file is the rules file — deny-listed on purpose, and editing it to match the code is backwards. The real order now lives in an executable file, the one place it cannot go stale without something failing.
admin/versions/v0.5.0.md — this note as markdown, which is what it is. |
| v0.4.0 | 2026-09-14 | Names get addresses. 198 entities — people, organisations, institutions, publishers, topics, places — each with a page at entidades/<type>/<id>/, and the first mention of each one on any page of the paper now links there. 313 links. An entity page has not one line written about it: it has the source's verbatim fields, the edges read aloud through the reading published in the ontology — «o evento lista X sob Y» — and a count of which frozen file the name appears in and how often. This is §6 paying for itself: it is what made refusing relacionado_com worth it. The linking formula is published, because it is a classification. Verbatim name with the accents the bytes gave it, at least 6 characters, word boundaries, the first mention on a page and no other, never inside another link. And only types that name a thing of their own: linking the word "hardware" to a Technology page would be saying that word is a reference to that node, and it is not — it is the word. A link like this says "the text contains this name", never "the text is about this entity". The graph starts knowing WHOSE each byte was. Every frozen source gains a publicada_por edge to the house that publishes it, derived from the register by the editor_de_fonte formula. A new type, Editor, rather than widening Instituição: that type's published definition is "a public body, a regulator or a research unit", and a newspaper is none of those — widening a definition people have already read changes what it promises. And two names still different after the formula stay two nodes: if the register says «Diário da República» and «Diário da República Eletrónico», those are two things it says. Two grounds for a link, and the page says which. The strong one is the name being in the bytes (179 entities). The weak one is being the name our own register gives a source's publisher (4) — it exists because «Diário da República» publishes two frozen sources and appears in no byte at all, since that register's home page returns 22 visible characters to an automatic reader, which is one of this paper's own stories. The two are not conflated: the field says which, the page explains it, and gate 22 fails the build if anyone invokes the weak one while holding the strong one. Four new gates (21 to 24), ten injection tests. Two of the tests caught defects in the gates themselves before catching anything in the site, and gate 24 — "no Pessoa page says anything about the person" — had to be hardened when the organisation cell gained a link: a gate that stops seeing half of what it guards guards nothing. admin/versions/v0.4.0.md — this note as markdown, which is what it is. |
| v0.3.1 | 2026-09-14 | There were two document readers, and a site that says content exists once cannot have two. backoffice/viewer.html was written before the request to put the list on the left and the document on the right; once that was done in <pt-doc-browser>, there were two implementations of one thing — and two implementations diverge, always. No page linked to the old one, but deleting an address that once existed breaks whoever kept it: it becomes a permalink that forwards to docs.html, carrying the #fragment with it, so the requested document still opens. There is one reader. admin/versions/v0.3.1.md — this note as markdown, which is what it is. |
| v0.3.0 | 2026-09-14 | The title becomes the subject. «O Ecossistema Português de IA» is the masthead; the address becomes the subtitle. A reader arriving does not care about the domain — they want to know what it is about. And the front-page headlines become LINKS to their articles: a newspaper whose headline links to nothing is a poster, and that is what this was. A read-only API, in English, with a console. /api/ — 18 collections, 28 operations, 377 files. Every path is a file on disk: there is no server, so there is no verb but GET, no request body anywhere, and nothing that can fail in a way openapi.json does not predict. The paths are English on purpose — the intent is several languages over one set of data — and the KEYS inside the documents stay Portuguese, because they are the record this publication keeps of itself and translating them on the way out would serve something no file in the repository contains. The console makes the request from the browser and shows the bytes: a console that only describes is a document with extra steps. Web components, to the estate's contract. Four, in the form coding.sgit.ai documents: native, no framework, no build step, static jsUrl = import.meta.url so they locate themselves, onReady() rather than connectedCallback, and the versioned path v1/v1.0/v1.0.0/. Two departures, both forced by this site's own rules: the base class is hosted here rather than coming from a third-party CDN, because a page that needs a third party to render is a page a third party can stop rendering; and the triplet's markup file is .tpl and not .html, because here every .html is a PAGE to the site gate — it must carry a canonical, an agent block and a sitemap line — and a component fragment is none of those. The alternative was weakening the gate, and the gate is deny-listed on purpose. Documents become readable without losing your place. Tree on the left with folders that collapse — briefs/pack/ is one element, not thirty rows — and the document on the right, on white, like a document. References to the rest of the estate sit at the foot of the tree. Before, it was a list that threw the reader onto another page and took the others away. An article's files open in a viewer. Clicking afirmacoes.json stops returning a raw file: states get colour, a source id links into the register, a SHA-256 is shortened. The raw file stays one click away. Paying to read, demonstrated rather than argued. One cent a page, five euros in the wallet, a top-up at zero, and the ledger in view. It charges nobody anything and says so in the panel's first sentence — a paywall that would not admit to being a demonstration would be the one dishonest thing on a site whose entire argument is provenance. And it never blocks: the interesting part is the ledger, not the lock. The declaration stops being a block on every page and becomes one line with a page behind it. Repeated thirty times, a notice stops being read — the reader learns the shape of the block and skips it, which is the opposite of what a notice is for. /proveniencia/ owns what this is: written by AI agents, from several providers, with light curation by a named human editor. Saying "light" is more honest than leaving a line-by-line review implied. admin/versions/v0.3.0.md — this note as markdown, which is what it is. |
| v0.2.0 | 2026-09-14 | An article becomes a dated folder. artigos/<yyyy>/<mm>/<dd>/<slug>/ with four files: the prose, the verification record for each claim, the provenance — which run by which agent produced it, in what order — and the JSON saying what the article is. A folder and not a file because an article is not only the text: it is the text plus the bytes it stands on plus whoever re-read them plus whoever wrote it, and in a single file three of those four end up elsewhere and stop agreeing. The date in the path is the date of the material, not of publication. Six folders opened: three with prose and verification done, three gathering material for the stories §11 of the brief commissioned. None published — that line is the editor of record's. dados/historias.json becomes DERIVED from the folders, as the organisations are derived from the speaker cards. Each of the eight sections gains a folder and an editorial record. seccoes/<id>/seccao.json says what the section covers, what it can and what it CANNOT claim today, which sources it has frozen and which did not resolve, and which questions are open. The two sections with not one frozen source are forced by gate 20 to answer «Nada.» to the question of what they can claim. A back office, in English. /backoffice/: the operations console with the pipeline's state, who may write where, the board, the articles, each section's coverage, the runs and the commits — and a reader for this repository's 32 markdown documents, reading them from their own bytes rather than a copy. It is in English by the editor's decision, and the condition of that exception is checked: gate 19 fails the build if a back-office page cites a frozen source as evidence. The console reports the newsroom, never the world. Five new gates, in a separate file. build/gates_artigos.py exists apart because build/gates.py is in the deny list of .claude/settings.json: an agent that can edit the gate that stops it has no gate at all, and the wrong way to add checks would be to lift that protection. All five were injection-tested, like the others. Two rendering defects fixed, and both are worth naming: the markdown renderer made every LINE a paragraph, which broke prose mid-sentence and left a **bold** crossing the break without closing; and article prose had no vertical rhythm at all because .std has zero margin — right on the front page, where the spacing comes from the column's gap, and wrong in an article, which has no column. admin/versions/v0.2.0.md — this note as markdown, which is what it is. |
| v0.1.2 | 2026-09-14 | The loop closes in view. The deliveries page starts showing where a delivery comes from — the two research briefs an outside assistant receives — beside what happened when it came back. They are linked from the repository and not republished as pages, by the same principle that governs the frozen copies: this site links, it does not reproduce, and content exists once. There are two briefs and not one because the tools fail in different ways, and the page says so: one cites well by default and searches well in Portuguese; the other structures better and is weaker at guaranteeing it actually opened each address — which is precisely the weakness the excerpt gate catches. admin/versions/v0.1.2.md — this note as markdown, which is what it is. |
| v0.1.1 | 2026-09-14 | The approval step stops being folklore. Each delivery's page starts showing exactly how the editor of record approves or rejects an item — with that delivery's REAL identifiers, not an example with invented names the editor would have to translate before using, which is where identifiers get mistyped. The item suggested for approval is the one with the most claims whose excerpt is in the bytes; the one suggested for rejection has none. A new skill: /newsroom-entregas. The review session for a delivery, written for the editor: receive the file without touching it, freeze what it names, check every excerpt, and decide item by item. It also says what to do when a source is not readable but should be — the Diário da República PDF route — and why a 403 is not worked around. A rule that was in the gate and written nowhere the editor would read it: you do not approve an item whose claims are all without a readable source. Approving means "this may be written as fact", and in that case there are no bytes to hold it up. The route is to open a research issue, not to lower the bar. admin/versions/v0.1.1.md — this note as markdown, which is what it is. |
| v0.1.0 | 2026-09-14 | The MVP. The ingestion path running against real sources (fetch, freeze, hash, extract, diff), the graph with Portuguese verbs, the data-protection notice, the front page as the design fixes it, the newsroom desk, and the review flow for research deliveries. Three decisions recorded rather than left implicit. (1) The release branch is dev, which is also this repository's default branch — the pack requires the release branch to BE the default branch, because a scheduled run clones the default branch, and it called that branch main only because it was written before this repository existed. (2) There is one stylesheet, and the front page uses it like every other page: the pack allows two, and two would diverge. (3) Graph readings that touch a person are gender-invariant, because this site does not know anyone's gender and inferring it from a name would be an inference about a named person. What this version is NOT. No story published — the first three are issues in «procurado», and publishing is the editor of record's line. Three of the eight sections with not one node, and saying so on their own pages. No agent level beyond A tested. Nothing from any research delivery appears on a reading page: gate 13 fails the build if it does. One capability worth building. The Diário da República renders by script and returns 22 visible characters to an automatic reader — but it publishes its instruments as PDFs, encrypted with the standard security handler and with subsetted fonts, which makes a generic library return zero characters in silence. build/pdf.py decrypts them and reads each font's /ToUnicode map. Without it, this newsroom would have published "the national register cannot be read" when the truth was "we had not implemented the decryptor". admin/versions/v0.1.0.md — this note as markdown, which is what it is. |