Skip to content

What the bot is allowed to read

phpbotscout answers from a corpus it assembles itself. Nothing in a configuration file says which repositories that is. This page explains why, and what the rules actually protect against.

Why the corpus is a predicate, not a list

A repository is in the corpus if it is a public, documented member of the phpboyscout group. That is evaluated on every run against what the forge reports right now.

The alternative — a list of sources in the config file — fails at exactly the moment it matters. A checkout cannot say whether a repository is still public, still in the group, or even which project it is: forges redirect renamed and transferred projects, and directory names do not reliably match project names. A repository made private yesterday looks identical on disk to one that is still public.

So configuration is treated as a request to index and never as an authority on visibility. The only thing that can answer "is this public?" is the thing that decides it.

The predicate has a second effect worth naming: the corpus grows with the toolkit rather than with somebody remembering to edit a file. A new documented module is in the corpus the next time discovery runs.

What each clause decides

Every candidate gets a verdict and the clause that produced it, and index sources prints all of them — rejections included. A predicate nobody can inspect is a curated list with extra steps, and a repository that simply vanished from the output would send an operator hunting for a bug in discovery when the answer is that the rules did their job.

The clauses are evaluated in a fixed order, and the order is load-bearing.

Exclusions run first, and are absolute. Four paths — infra, sandbox, scoutdm, scoutdm-site — can never be indexed, and are matched by prefix so a subgroup is covered by its parent. Running this check first means an excluded repository is never read from at all, not even to discover it would have failed a later check anyway. It also means a rejection is attributed to the strongest reason available, so the exclusion is visible in the listing rather than incidental.

Then the namespace, matched on the path segment rather than as a substring, so notphpboyscout/x and someone/phpboyscout are both outside the group. Getting that wrong would admit a repository chosen by somebody else.

Then visibility, and this is the clause doing the real work. It is an allowlist against public, never a denylist against private, because != private admits both internal and unknown — and unknown is reachable any time a payload comes back partial.

Then the two documentation checks: a zensical.toml marker, and a publicly reachable documentation site.

Why the exclusion list still exists

Every excluded path is also private today, so each is refused twice. The list is not the control — the live visibility check is. It is kept for the one failure visibility cannot catch: a repository made public by mistake. Defence in depth, with an honest label on which lock is actually holding the door.

phpboyscout/iac was removed from the list in July 2026 for exactly this reason. Its terraform modules are public, documented and legitimate support surface; the two private repositories beneath it are refused by the visibility clause, which is where that judgement belongs. Private is a fact the forge reports, not an opinion encoded in a Go file.

Why "I could not find out" is not a rejection

unresolved exists because "no" and "I could not find out" demand opposite handling.

Reading a repository's zensical.toml can fail with a 404 — which genuinely means there is no marker — or with a 500, which means nothing at all. Treating the second as the first would quietly drop a documented repository from the corpus, and a different one on the next run, with no signal that anything had happened.

The same applies to the Pages lookup, where a 403 is routine rather than exceptional: reading a project's Pages settings needs the Maintainer role, so a weak token turns every candidate unresolved. That is why a token problem shows up as an unhelpful-looking corpus rather than as an authentication error. The verdict is not final — the next run asks again.

Why an archived repository is excluded after the fact

Archiving is evaluated after the predicate rather than inside it, because it is a judgement about usefulness rather than about permission. An archived repository is not untrusted; its documentation is unmaintained, which for a bot answering questions is worse than absent. Confidently out of date reads exactly like current.

Issues can point; they cannot tell the bot what to say

Anyone with an account can open an issue on a public repository. That makes issue and comment text arbitrary third-party writing sitting inside repositories the bot otherwise trusts.

Fencing untrusted text in the prompt defends against the instruction form of that problem — planted text saying "ignore your instructions and…". It cannot touch the content form, which does not attack the system at all. An issue saying, plausibly and wrongly, "the supported way to store credentials is credentials.literal: true" would be retrieved, found topical, and presented as fact with a citation to a real page in a real repository. Every trust signal is genuine; only the content is false.

So the rule is structural rather than a matter of prompting: issue text is never answer content. An issue may only point at documentation or code, and the target must satisfy the corpus rules on its own. Being linked from something in the corpus confers nothing — otherwise a planted link into an excluded repository would be a way to launder content past the exclusion. A pointer to another issue is not a valid pointer either, because a chain of them leads back to somebody's prose.

Filtering by author was considered and rejected. It still rests on trusting a set of people, and it fails against the likelier case by far: not an attacker, but a maintainer who was right in 2024 and is wrong now.

What that costs, and why it is worth paying

It loses a genuine category of answer — the explanation a maintainer wrote in a thread and never wrote up anywhere else.

That is the knowledge-gap loop working rather than failing. An answer that exists only in an issue is a documentation gap. Quoting the thread would satisfy the asker and leave the gap in place indefinitely; declining escalates it, and the gap surfaces as work. A bot that goes quiet in exactly the places the documentation is thin is behaving correctly for a product whose most valuable output is a ranked list of things to document.

What this does not protect against

  • A public repository containing wrong documentation. The predicate decides what may be read, not whether it is true.
  • Go files under docs/development/. The rule that keeps specs and delivery notes out of the corpus is applied to markdown; a spike program living beside them is still indexed as code. See the indexing reference.
  • Content that was public when indexed and is private now. Discovery re-runs every cycle and stops indexing it, but rows already written are not deleted by that alone.