Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
python:
name: Python (pytest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
Expand All @@ -29,15 +38,16 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -r "${PROJECT_DIR}/requirements.txt"
- name: Verify CI discovery and patch hygiene
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python "${PROJECT_DIR}/scripts/verify_ci_discovery.py" --base "${BASE_SHA}"
- name: Verify Louvain dependency contract
run: |
python "${PROJECT_DIR}/scripts/verify_louvain_dependency_contract.py"
run: python "${PROJECT_DIR}/scripts/verify_louvain_dependency_contract.py"
- name: Verify docs hygiene
run: |
python "${PROJECT_DIR}/scripts/verify_docs_hygiene.py"
run: python "${PROJECT_DIR}/scripts/verify_docs_hygiene.py"
- name: Verify frontend/backend API contracts
run: |
PYTHONPATH="${PROJECT_DIR}" python "${PROJECT_DIR}/scripts/verify_api_contracts.py"
run: PYTHONPATH="${PROJECT_DIR}" python "${PROJECT_DIR}/scripts/verify_api_contracts.py"
- name: Verify clusters (fixture, granularity 25)
run: |
PYTHONPATH="${PROJECT_DIR}" python "${PROJECT_DIR}/scripts/verify_clusters.py" \
Expand All @@ -47,16 +57,14 @@ jobs:
PYTHONPATH="${PROJECT_DIR}" python "${PROJECT_DIR}/scripts/verify_clusters.py" \
--base-path "${PROJECT_DIR}/tests/fixtures/medium_graph/graph_snapshot" --granularity 40
- name: Pytest (excluding selenium + supabase-dependent suites)
# `-m 'not selenium and not requires_supabase'` honours the markers
# declared in pytest.ini. Selenium tests need a real browser; the
# supabase tests need a network secret we don't expose in CI.
# These suites need a real browser or a network secret unavailable in CI.
run: |
PYTHONPATH="${PROJECT_DIR}" python -m pytest "${PROJECT_DIR}/tests" \
-m 'not selenium and not requires_supabase' \
-q --tb=short --maxfail=20

public-site:
name: public-site (vitest)
name: public-site (vitest + build)
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -72,9 +80,11 @@ jobs:
run: npm ci --no-audit --no-fund
- name: Vitest
run: npx vitest run --reporter=default
- name: Production build
run: npm run build

graph-explorer:
name: graph-explorer (vitest)
name: graph-explorer (vitest + build)
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -89,7 +99,7 @@ jobs:
- name: Install
run: npm ci --no-audit --no-fund
- name: Vitest (unit suite only; skip e2e/playwright)
# graph-explorer includes Playwright e2e under ./e2e — those need
# a browser and run via `npx playwright test`. CI here covers the
# vitest unit suite under ./src.
# Playwright e2e needs a browser; this job covers Vitest under ./src.
run: npx vitest run src --reporter=default
- name: Production build
run: npm run build
49 changes: 48 additions & 1 deletion tpot-analyzer/docs/EXPERIMENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,54 @@

> Hypotheses tested, results observed, lessons learned. This is institutional memory — what we tried, what worked, what didn't, and why. Each entry records the question, the method, the data, and the verdict so future sessions don't re-run failed experiments or miss validated insights.

*Last updated: 2026-08-23 (repository preservation and canonical-main consolidation)*
*Last updated: 2026-08-23 (hosted CI discovery and main protection)*

---

## EXP-046: Does relocating the existing gate make CI discoverable and enforceable?

**Date:** 2026-08-23

**Question:** Is the absence of GitHub Actions runs caused by the nested
workflow path, and can the existing Python/Node contract be made discoverable
while adding production-build and patch-integrity coverage?

**Hypothesis:** Confidence `0.97` that moving `test.yml` to repository-root
`.github/workflows/` will expose three stable status contexts. Expected local
red signal: root workflow missing and nested workflow present. Expected green
signal: the discovery verifier recognizes all three jobs and exact runtime,
install, test, build, permission, history, and hygiene contracts. Hosted
falsifiers are any missing workflow/run/job or any job that fails on a real PR.
Fallback is to keep `main` unprotected, diagnose the PR branch, and never waive
or rename a failing contract merely to make it mergeable.

**Method:** Added a human-readable verifier first, ran it against the nested
layout, corrected its conflict-marker matcher after it falsely matched long
documentation separators, then relocated the workflow. Preserved Python 3.11,
Node 22, credential-free pytest markers, API/docs/Louvain/cluster gates, and
added both frontend production builds, read-only permissions, full history,
concurrency cancellation, exact conflict markers, and changed-patch whitespace.

**Result (local checkpoint):** The valid pre-move verifier passed `3/5`; only
root-workflow presence and nested-workflow retirement failed. The post-move
verifier passed `20/20`. Louvain passed `2/2`; docs hygiene `9/9`; API contracts
found `23` frontend paths / `77` routes / `0` gaps; both cluster fixtures
passed; Python passed `1,768` with `5` skipped; public site passed `212/212`;
graph explorer passed `791/791`; both production builds passed under cached
Node `22.23.1`. Existing non-failing warnings remain visible. Hosted push run
`32629747636` and pull-request run `32629769099` both succeeded at `97e71f1`;
each exposed and passed exactly `Python (pytest)`, `public-site (vitest +
build)`, and `graph-explorer (vitest + build)`. **Hypothesis confirmed.**

**Lesson so far:** A verifier must distinguish new-patch defects from tolerated
historical formatting debt. An all-tree whitespace scan on a branch's first
push would fail thousands of pre-existing lines, so the first-push case checks
`HEAD^..HEAD`; pull requests use their exact base SHA and direct subsequent
pushes use the event's prior SHA.

**Next step:** Merge PR #8 only after the evidence-only follow-up run is green,
then require the three observed contexts and pull requests on `main` while
retaining administrator bypass during initial rollout.

---

Expand Down
10 changes: 6 additions & 4 deletions tpot-analyzer/docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,12 +816,14 @@ propagation out of TPOT to mainstream (no data on journalists/policymakers).

## Infrastructure & Tooling

- [ ] **Make GitHub discover the CI workflow** — Actions is enabled, but
- [ ] **Make GitHub discover and enforce the CI workflow** — Actions is enabled, but
`gh workflow list` returned no workflows after the 2026-08-23 main push
because `test.yml` lives under `tpot-analyzer/.github/workflows/` instead of
repository-root `.github/workflows/`. Move or add a root workflow while
preserving the current project-directory detection and exact Python/Node 22
commands; confirm a real push run before calling hosted CI green.
repository-root `.github/workflows/`. The repair branch relocates it while
preserving project-directory detection and exact Python/Node 22 commands,
adds both production builds and patch hygiene. Push run `32629747636` and PR
run `32629769099` passed all three jobs; completion now requires merging PR #8
and requiring those observed checks and pull requests on `main`.
- [ ] **Retire the 2026-08-23 consolidation recovery assets deliberately** —
choose a retention window, then use `verify_repository_consolidation.py`
before removing any dated preservation ref, worktree, parked stash, bundle,
Expand Down
44 changes: 44 additions & 0 deletions tpot-analyzer/docs/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Worklog - TPOT Analyzer

## Hosted CI Discovery and Main Protection (2026-08-23)

- [2026-08-23 14:08 IST] **Made the existing gate discoverable and added build
coverage; hosted proof is the remaining falsifier (Codex GPT-5)**
- **Hypothesis / fallback:** confidence `0.97` that Actions had no workflow
because `test.yml` was below the repository discovery root, not because
the existing test contract was invalid. A missing workflow, absent job,
failed production build, or status context that cannot be required on
`main` is a falsifier. Fallback is to leave branch protection unchanged
and repair the branch through the PR rather than bypass a failing check.
- **Workflow (`/.github/workflows/test.yml:1-105`):** relocated the existing
three-job contract to the repository root, retained Python 3.11 and Node
22, preserved the credential-free pytest selection and fixture/API/docs
checks, added both production builds, full checkout history, concurrency
cancellation, and read-only contents permission.
- **Verification (`scripts/verify_ci_discovery.py:1-153`):** the first valid
execution failed `2/5` discovery invariants exactly because the root
workflow was absent and the nested workflow remained. An earlier marker
scan falsely treated long documentation separators as conflicts; the
exact seven-character marker pattern fixed that verifier defect before
accepting the baseline. After relocation the gate passed `20/20`,
including workflow contracts, conflict markers, and working/staged patch
whitespace. The all-zero first-push SHA uses `HEAD^` rather than scanning
historical repository whitespace; PRs still compare against their exact
base SHA.
- **Local gate:** Louvain `2/2`; docs hygiene `9/9`; API contracts `23`
frontend paths / `77` backend routes / `0` gaps; both cluster fixtures;
Python `1,768 passed, 5 skipped`; public site `212/212`; graph explorer
`791/791`; both production builds green. Clean installs used cached Node
`22.23.1`. The existing 20 SciPy warnings, React `act(...)` warnings,
canvas debug output, dynamic-import notice, and chunk-size warning remain
visible rather than being suppressed in a CI-only change.
- **Hosted proof:** push run `32629747636` and pull-request run
`32629769099` both completed successfully at commit `97e71f1`; each ran
`Python (pytest)`, `public-site (vitest + build)`, and `graph-explorer
(vitest + build)`. The PR run completed those jobs in 1m41s, 30s, and 51s
respectively. This confirms discovery, both event triggers, the exact
check names, and successful clean hosted installs/builds—not merely local
equivalence.
- **Remaining gate:** merge PR #8 after its evidence-only follow-up run,
then require those three observed checks plus pull requests on `main`.
Administrator bypass remains enabled initially so an incorrect new rule
cannot lock maintainers out.

## Repository Preservation and Canonical-Main Consolidation (2026-08-23)

- [2026-08-23 13:10–13:42 IST] **Preserved, classified, integrated, and verified
Expand Down
153 changes: 153 additions & 0 deletions tpot-analyzer/scripts/verify_ci_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Verify that GitHub can discover and enforce the portable CI contract."""
from __future__ import annotations

import argparse
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path


PROJECT_ROOT = Path(__file__).resolve().parents[1]
REPOSITORY_ROOT = PROJECT_ROOT.parent
WORKFLOW = REPOSITORY_ROOT / ".github/workflows/test.yml"
NESTED_WORKFLOW = PROJECT_ROOT / ".github/workflows/test.yml"
EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"


@dataclass
class Report:
passed: int = 0
failed: int = 0

def check(self, label: str, ok: bool, detail: str) -> None:
print(f"{'✓' if ok else '✗'} {label}: {detail}")
self.passed += int(ok)
self.failed += int(not ok)


def run(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
args,
cwd=REPOSITORY_ROOT,
capture_output=True,
text=True,
check=False,
)


def valid_base(revision: str) -> tuple[bool, str]:
if revision == "0" * 40:
parent = run("git", "rev-parse", "--verify", "HEAD^")
if parent.returncode == 0:
return True, parent.stdout.strip()
return True, EMPTY_TREE
if not re.fullmatch(r"[0-9a-f]{40}", revision):
return False, revision or "empty"
exists = run("git", "cat-file", "-e", f"{revision}^{{commit}}")
return exists.returncode == 0, revision


def verify_patch_hygiene(report: Report, base: str | None) -> None:
marker_scan = run(
"git", "grep", "-n", "-I", "-E",
r"^(<<<<<<< |=======$|>>>>>>> )",
"--", ".",
)
report.check(
"tracked conflict markers",
marker_scan.returncode == 1,
marker_scan.stdout.strip() or "none",
)
for label, args in (
("unstaged whitespace", ("git", "diff", "--check")),
("staged whitespace", ("git", "diff", "--cached", "--check")),
):
result = run(*args)
report.check(label, result.returncode == 0, result.stdout.strip() or "clean")
if base is None:
return
ok, resolved = valid_base(base)
report.check("base revision", ok, resolved)
if not ok:
return
result = run("git", "diff", "--check", f"{resolved}..HEAD")
report.check(
"committed patch whitespace",
result.returncode == 0,
result.stdout.strip() or f"{resolved[:12]}..HEAD clean",
)


def verify_workflow(report: Report) -> None:
report.check("root workflow exists", WORKFLOW.is_file(), str(WORKFLOW))
report.check(
"nested workflow retired",
not NESTED_WORKFLOW.exists(),
str(NESTED_WORKFLOW),
)
if not WORKFLOW.is_file():
return
text = WORKFLOW.read_text(encoding="utf-8")
checks = (
("push trigger", " push:" in text),
("pull-request trigger", " pull_request:" in text),
("read-only contents permission", " contents: read" in text),
("full history for patch gate", "fetch-depth: 0" in text),
("Python 3.11", "python-version: '3.11'" in text),
("Python test job", "name: Python (pytest)" in text),
("public-site test job", "name: public-site (vitest + build)" in text),
("graph-explorer test job", "name: graph-explorer (vitest + build)" in text),
("credential-free pytest selection", "not selenium and not requires_supabase" in text),
("API contract verifier", "verify_api_contracts.py" in text),
("docs hygiene verifier", "verify_docs_hygiene.py" in text),
("CI discovery verifier", "verify_ci_discovery.py" in text),
)
for label, ok in checks:
report.check(label, ok, "present" if ok else "missing")
node_job_count = text.count("node-version: '22'")
npm_install_count = text.count("npm ci --no-audit --no-fund")
build_count = text.count("run: npm run build")
report.check(
"Node 22 jobs",
node_job_count == 2,
f"count={node_job_count}",
)
report.check(
"clean npm installs",
npm_install_count == 2,
f"count={npm_install_count}",
)
report.check(
"production builds",
build_count == 2,
f"count={build_count}",
)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--base",
help="40-character base commit for a committed-range whitespace check",
)
args = parser.parse_args()
report = Report()
print("CI Discovery Verification")
print("=" * 25)
verify_workflow(report)
verify_patch_hygiene(report, args.base)
print(f"\nResult: {report.passed}/{report.passed + report.failed} checks passed")
print("Next steps:")
if report.failed:
print("1. Fix the first failed discovery or patch-hygiene invariant.")
print("2. Do not configure required checks until a real PR run is green.")
else:
print("1. Push a branch and confirm all three hosted jobs complete.")
print("2. Protect main using the check names observed on that run.")
return 1 if report.failed else 0


if __name__ == "__main__":
raise SystemExit(main())
Loading