Repository navigation
0.13.0
Additive: the predicates of the three optional packages move into the core, so that an adapter can ask "is
indexnowkit/sitemap installed?" without loading a class that lives in indexnowkit/sitemap.
Added
Adapter\OptionalPackage::sitemap(),verify(),history()(each?bool $installed = null): the Composer names,
the marker classes (Sitemap\SitemapReader,Verify\PageSignals,History\HistoryConfig, written as strings — the
core names no class of a package it does not require) and the feature words of the family's optional packages, in
the core.Sitemap\Adapter\SitemapServices::package(),Verify\Adapter\VerifyServices::package()and
History\Adapter\HistoryServices::package()delegate to them (sitemap 0.7.1, verify 0.3.1, history 0.3.1); an
adapter calls the core's method directly. Before, the Symfony bundle, the Laravel and the Yii2 adapters called the
packages'*Services::package()unconditionally, which is aClass "IndexNowKit\Sitemap\Adapter\SitemapServices" not foundat the first configuration build of an application without the package (symfony-bundle 0.14.1, laravel 0.14.1,
yii2 0.13.1 fix it; the tests of wave I passedinstalled: falsewith the packages still installed, so they did not
catch it).docs/adapters.md§2 "Optional packages" names the new recipe;docs/bc.mdlistsAdapter\OptionalPackage
in the "call" tier, where it always belonged.
Then the six-lens audit of 0.13 (docs/plans/audit-0.13.md in the specification workspace), run on this wave before it
was tagged. Everything below went into the same unreleased minor, so there is no 0.13.0 in the wild without it. Two of
the findings lost URLs silently: an insert whose verifier compared a DECIMAL column never announced the new page, and a
dry_run given as the string "false" read as true. The rest is the shape of the API the next adapter is written
against, plus the four constructions every adapter had copied.
Added
- What every router bridge, queue dispatcher and locale check of the family had copied, as core classes (wave M,
spec 19).Url\RouteOrigin(static, "call" tier):expand()— the locales a rule generates for,'all'over the
adapter's list, with one warning per process when that list is empty (the rule silently collapsing to one URL was
logged by the Laravel bridge only; the other three now say it too);pinnedRoot()—hosts.<host>.base_url, else
https://<host>;rebase()— scheme, host and port of an origin over the path, query and fragment of a generated
URL;generationFailed()andnoRequestHost()— the twoConfigurationExceptions, one text each (four bridges had
four spellings ofCannot generate route).Dispatch\BatchingDispatcher(Closure $enqueue, Config, ?Logger, string $noun = 'job'): one unit perbatch.max_urls,newJobId()(twelve hex characters: three adapters had
bin2hex(random_bytes(6))each), a push that throws logged ascannot queue {count} URL(s) ({noun} {id}), they are lostwithConfig::logSample()and never rethrown; the adapters' dispatchers are the push closure around it.
Check\LocalesCheck($locales, $rules, Closure $classes, $option = 'router.locales', ?string $parameter = null), code
router.locales: the one check three adapters printed with three texts — the warning names the option and up to
three classes ("and N more"), the ok line names the locales and the route parameter when the adapter gives one, a
class whose rules cannot be read is skipped.Check\DispatchLine::describe($dispatch): thesync/none
sentence of the adapters' queue checks. Debounce\DebounceStoreFactory::isShared(?string $store): whetherdebounce.storenames a cache shared by
every process, the question nine places across five packages answered with their ownin_array($store, [MEMORY, NONE], true)(the bundle with the literals).Check\DebounceStoreCheck::PROBE_KEY(indexnowkit_check): the
key every adapter's probe writes — two adapters wroteindexnowkit:check, and:is a character PSR-16 reserves,
so a strict cache (yiisoft/cache) refused the probe andcheckreported a working store as broken. The docblock of
the check says "writes a test key" now; two probes only read one.Adapter\OptionalPackageknows the options its package owns: a fifth constructor argument,?string $optionsProvider— the name of the package's*Services::options()as a string, so the core still names no class
of a package it does not require — filled bysitemap(),verify(),history();options()calls it only when
installed()says yes; the staticownedOptions(list<OptionalPackage>)andignoredBlocks(list<OptionalPackage>)
fold a list of predicates into the two arguments ofAdapter\ConfigFactory, which three adapters built from eight
identical lines each. A fourth package is one line in the core now, not three edits in three adapters.Http\Psr18Transport::discover()andHttp\TransportFactory::lazy()take the application's PSR-17 factories
(?RequestFactoryInterface $requestFactory,?StreamFactoryInterface $streamFactory, appended and optional): a
PSR-7 stack with aPsr17Factoryservice of its own (Slim, Mezzio, Yii3) hands it over instead of having
php-http/discoveryfind one;nulldiscovers, as before. The docblock ofsendRequest()says why the two PSR-18
exception subtypes are not told apart;docs/adapters.md§12 says what the transport does about redirects (nothing
the standard does not: off on the clients it builds, the application's own client keeps its defaults) and §13 why the
core takes PSR-16 only (wrap a PSR-6 pool in the view your ecosystem ships) and §11 which key-file recipe each stack
gets.Testing\ArrayLogger's docblock says it does not check the level — a double, not a PSR-3 logger.Key\KeyFileRequestHandler(wave L, spec 18 §3.5):GET /<key>.txtover PSR-7 for any PSR-15 stack — a
RequestHandlerInterface(the action of a route: the key is read off the path), aMiddlewareInterface(in front
of the router: a request it does not serve goes on to the next handler untouched) andrespond(?string $key, $request)
for a key the router already extracted. The body isKeyFileResponder's, the headers areConfig::keyFileHeaders(),
the host is the request URI's;fromConfig($config, $keys, $responseFactory, $streamFactory)builds it over the
adapter's key provider and PSR-17 factories. Nothing changes in the semantics — only the PSR-7 response is built in
one place instead of in every adapter (the Yii3 handler delegates to it).psr/http-server-handlerand
psr/http-server-middlewarejoinrequire(interfaces only; the core stays PSR-only).Adapter\SubmitterFactorytakes?ClockInterface $clockas its last parameter and passes it to theSubmitter
it builds;Adapter\Services::submitterFactory()gives it the graph'sclocknode (A1). Appended and optional, so
the "call" tier holds. Without it theclocknode stopped at the application's submitter:Testing\FrozenClockleft
submit,submit-recordandsitemapon the wall clock, the submission history recorded commands and requests on
two different clocks, and the debounce window was counted twice over.Adapter\Services::requireRouter()andrequireResolverLocator()(A9): the same nodes asrouter()and
resolverLocator(), but typed non-nullable and throwing aConfigurationExceptionthat names the missing node. An
adapter that always wires one wrote?? throwwith a message nobody ever read; two adapters had shipped that dead
branch before this existed.- The
eventsclosure ofAdapter\ServicesBuildermay returnnull(A8), like the nullable nodes. An adapter
whose container may or may not hold aPsr\EventDispatcher\EventDispatcherInterfacenow answers that inside the
closure instead of asking the container duringbuild(), which is documented to do no IO. Check\SampleGateCheckoverCheck\SampleOptions(A2): the--sample/--sample-classgate in front of the
optionalindexnowkit/verify, in the core because it has to load without the package. All four adapters carried a
byte-identical copy of the class, the holder and the two user-facing sentences. Every line it writes carries the code
verify.installed, which is what the adapters always printed —docs/check-codes.mdhad documentedverify.sample
for it, a code that exists only while the package does (D1).Hook\ObserverHelper::forKit()next toforChanges()(A23): the plain case as a named constructor, so the
union-typed constructor with its conditionally required$sinkis no longer the way in.Transaction\VerifyingStaging::stage()takes a subject key (?string $key, appended) and merges a later change
of the same subject into the entry already staged, keeping both sets of URLs and the newer verifier (R4). Without it
two writes to one row inside one transaction staged two entries, both verified at the end against the last state
of the row, so the first one's URLs were dropped as "did not land" — including the old URL of a two-step rename.Url\ArrayResolverLocatorwraps a throwing container lookup in aConfigurationExceptionnaming the id
(IndexNow URL resolver "%s" cannot be built by the container: %s) (A10). Three adapters had copied that try/catch
with three different sentences and the fourth had none at all, so a container error escaped it raw, past
IndexNowException. Adapters handlocate:a bare lookup now.
Fixed
- PHP 8.5 without deprecations, phpstan on PHP 8.4+ clean —
Url\ViaWalkuses theSplObjectStorageoffset methods
(attach()/contains()are deprecated since 8.5),Url\UrlNormalizerdecodes a percent-escape withhex2bin()and
Attribute\RuleCompiler::compile()is@template T of objectover itsReflectionClass<T>(the newest stubs make the
template invariant). No runtime change. Transaction\VerifyingStaging::rowMatches()compares only unambiguous values — integers, strings, booleans,
backed enums and null — and treats an empty expectation as "the row exists" (R1). The row comes back raw from the
driver, so19.9written by the application met'19.90'from a DECIMAL column, atimestamptzcame back with a
zone suffix, and JSON came back in the database's own spelling. Inserts staged every non-null property, so one
such column dropped the announcement of every new page of that class, atdebuglevel. Floats, dates, arrays and
objects are now skipped like a column the row does not carry.Config::fromArray()parsesdry_run,enabled,strict_hostsandcollector.detect_leaksthrough the same
boolean parser as every neighbouring key instead of casting with(bool)(W1). An adapter that hands an environment
variable straight tofromArray()readINDEXNOW_DRY_RUN=falseastrue, anddryRunExplicitas true with it, so
checkreported a deliberate choice while production submitted nothing.Config::baseHost()returns the host as punycode, lower-cased (S4), the same formUrl\UrlNormalizerproduces.
With an internationalizedbase_urlandstrict_hoststhe two spellings never matched, so every URL of the site was
skipped withno_key— fail-closed, and with no line saying why.Config::keyFileHeaders()addsVary: Hostunderstrict_hoststoo, not only with ahostsmap (S3). With
strict_hostsand abase_urlthe key file answers 200 for the base host and 404 for every other one, so a shared
cache or CDN kept whichever of the two came first; a cached 404 turns every submission into a 403.Url\UrlNormalizernormalizes percent-encoding per RFC 3986 §6.2.2: an escape of an unreserved character
(A-Za-z0-9-._~) becomes the character, every other escape is upper-cased (W9)./%7Euserand/~userare one URL
now, debounced, submitted and recorded once.Retry\ForbiddenCounter::key()drops the brackets and the colons of an IPv6 host (S10). A host like[::1]put
a colon — a PSR-16 reserved character — into the cache key, so a strict cache rejected the read and the write: the
counter of that host degraded to one warning per 403 and thecriticalescalation never fired for it.
Changed
Config.phpis 600 lines instead of 1033 (audit 0.13 W11, deferred since 0.10;docs/plans/config-decomposition.md
in the specification workspace). Reading the array and the environment shapes moved toConfig\ConfigParser
(fromArray(),fromEnv(),serveKeyFileFrom(), the"3"/"true"scalar parsing,INDEXNOW_HOSTS), the
normalisation of the raw maps toConfig\ConfigNormalizer(hosts,engine_aliases,locale_hosts,logging.levels,
normalizer.tracking_params,production_environments, the URL predicates); both are@internal.Config::with()
builds its map of current values from the constructor's signature by reflection instead of a hand-written list of 44
names. Nothing on the surface changed: the same properties, constants, methods and messages —ConfigParityTestholds
the snapshot of all of them taken before the move (properties,toArray(), derived methods, the exception of every
invalid case) and stays as the regression guard.