291291# loop skipped everything. Verify against OpenRegister directly, by the same
292292# slugs the fixtures resolve by.
293293verify () {
294- python3 - " $1 " " $2 " << 'PY '
294+ python3 - " $1 " " $2 " " $APP_DIR " << 'PY '
295295import json
296+ import os
296297import sys
297298
298- path, kind = sys.argv[1], sys.argv[2]
299+ path, kind, app_dir = sys.argv[1], sys.argv[2], sys.argv[3]
300+
301+ # The hand-maintained floor: what the fixtures and workflow specs create and
302+ # read through /objects/openconnector/<schema>.
299303required = {
300304 'registers': ['openconnector'],
301305 # tests/e2e/workflows/_fixture.ts creates Sources / Mappings /
@@ -306,6 +310,66 @@ required = {
306310 'endpoint', 'consumer', 'event'],
307311}[kind]
308312
313+
314+ def manifest_schemas():
315+ """Every schema a manifest page BINDS A ROUTE TO.
316+
317+ 🔴 THE HAND-MAINTAINED LIST ABOVE CANNOT KEEP UP WITH THE MANIFEST, and on
318+ 2026-08-16 it did not: `SynchronizationRuns` (`/synchronization-runs`,
319+ schema `synchronization_run`, shipped by the `sync-run-progress.json`
320+ register fragment) was never in it. When that binding did not resolve on
321+ the CI instance, the seed said `schemas OK` and the failure surfaced two
322+ jobs later as a console assertion reading
323+
324+ Error fetching openconnector-synchronization_run collection: Proxy(Object)
325+
326+ — which names neither the schema nor the seed. A page whose route is
327+ declared but whose collection cannot be fetched is a seeding failure, and
328+ it belongs here, by name, before any spec runs.
329+
330+ Derived from the manifest rather than listed, so a page added tomorrow is
331+ covered without anyone remembering this file.
332+ """
333+ manifest_path = os.path.join(app_dir, 'src', 'manifest.json')
334+ try:
335+ with open(manifest_path, encoding='utf-8') as handle:
336+ pages = json.load(handle).get('pages') or []
337+ except (OSError, json.JSONDecodeError) as exc:
338+ # Not fatal: this is an ADDITION to the floor above, and a manifest we
339+ # cannot read must not take the checks that do work down with it.
340+ print(f'::warning::could not read src/manifest.json for schema '
341+ f'verification ({exc}); falling back to the hand-listed slugs.')
342+ return []
343+
344+ found = []
345+ for page in pages:
346+ if not isinstance(page, dict):
347+ continue
348+ config = page.get('config')
349+ if not isinstance(config, dict):
350+ continue
351+ # Only pages bound to THIS register — a page pointing at another app's
352+ # register is that app's to provision.
353+ if config.get('register') != 'openconnector':
354+ continue
355+ schema = config.get('schema')
356+ if isinstance(schema, str) and schema:
357+ found.append(schema)
358+ return found
359+
360+
361+ # ⚠️ ADVISORY, NOT A GATE — deliberately.
362+ #
363+ # The hand-listed floor stays a hard error: those slugs are what the fixtures
364+ # CREATE, so without them the suite cannot run at all. The manifest-derived set
365+ # is reported as a warning instead, because promoting it would turn today's ONE
366+ # failing spec into a failed seed and take the other 165 passing specs with it.
367+ # The job's verdict should keep coming from the specs; this exists so the log
368+ # NAMES the schema instead of leaving `Proxy(Object)` to be reverse-engineered.
369+ advisory = []
370+ if kind == 'schemas':
371+ advisory = sorted(set(manifest_schemas()) - set(required))
372+
309373with open(path, encoding='utf-8') as handle:
310374 raw = handle.read()
311375try:
@@ -318,7 +382,33 @@ except json.JSONDecodeError:
318382items = body if isinstance(body, list) else body.get('results', [])
319383slugs = {item.get('slug') for item in items if isinstance(item, dict)}
320384missing = [slug for slug in required if slug not in slugs]
321- print(f'[ci-seed] {kind} present ({len(slugs)}): {sorted(s for s in slugs if s)[:60]}')
385+
386+ # ⚠️ PRINT THE REQUIRED SLUGS, NOT A TRUNCATED DUMP OF EVERYTHING PRESENT.
387+ # This used to print `sorted(slugs)[:60]` beside a `len(slugs)` of 71 — an
388+ # instrument whose evidence disagreed with its own count, cut off alphabetically
389+ # at 'rule'. `synchronization_run` sorts after that, so the one slug in question
390+ # could not be seen either way, and reading the line gave a false answer in both
391+ # directions. The full count still gets printed; what gets ENUMERATED is the set
392+ # this check is actually about.
393+ present_required = [slug for slug in required if slug in slugs]
394+ print(f'[ci-seed] {kind}: {len(slugs)} present on the instance; '
395+ f'{len(present_required)}/{len(required)} required -> {present_required}')
396+
397+ if advisory:
398+ unbound = [slug for slug in advisory if slug not in slugs]
399+ if unbound:
400+ print(f'::warning::{len(unbound)} manifest page(s) bind a route to a schema '
401+ f'that is NOT on this instance: {unbound}')
402+ print('::warning::Each one\'s index page will fail to fetch its collection, '
403+ 'and the spec failure reads "Error fetching openconnector-<schema> '
404+ 'collection: Proxy(Object)" — which names neither the schema nor the '
405+ 'seed. That is what this warning is for.')
406+ print('::warning::Check that the register.d fragment declaring it is merged '
407+ 'into the descriptor AND that the import attached it to the '
408+ 'openconnector register.')
409+ else:
410+ print(f'[ci-seed] all {len(advisory)} manifest-bound schema(s) resolve.')
411+
322412if missing:
323413 print(f'::error::OpenConnector {kind} missing after import: {missing}')
324414 sys.exit(1)
0 commit comments