From 4b22e5e7de203070ab9706b983187bf4bac98bbc Mon Sep 17 00:00:00 2001 From: 2plot-network fan-out Date: Wed, 26 Aug 2026 21:28:16 +0000 Subject: [PATCH] sync: template 1.6.22-1.6.29 verbatim block (F3b fan-out) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec: sync/SYNC-1.6.22-1.6.29.md @ 5589318 (558931869fe3165565fc9e3b08e73a86d5261711) Mechanical whole-file copy of the spec's sync-verbatim block only. Contract/conditional items are NOT in this PR — see the spec. --- .claude/skills/report/SKILL.md | 7 ++++ .github/dependabot.yml | 58 ++++++++++++---------------- tests/test_auth_demos.py | 51 +++++++++++++++++++++++++ tests/test_claude_kit.py | 69 +++++++++++++++++++++++++++++++--- 4 files changed, 146 insertions(+), 39 deletions(-) create mode 100644 tests/test_auth_demos.py diff --git a/.claude/skills/report/SKILL.md b/.claude/skills/report/SKILL.md index 03a2e98..a75e5a0 100644 --- a/.claude/skills/report/SKILL.md +++ b/.claude/skills/report/SKILL.md @@ -39,6 +39,13 @@ Structure, in order: 6. **Open items** — split by who acts: owner (dashboard/env/merge), orchestrator (cross-repo), this repo's next pass. +Where the pass consumed a sync spec, per-item dispositions use +exactly these five words: `applied` / `ported-as-contract` / +`already-present` / `not-applicable-because` / `open`. `open` means +the detect fires but the item is deliberately out of this session's +scope — name it under Open items with who acts. Do not invent a +sixth word; the orchestrator's tooling reads these five. + Anti-patterns, all observed in the fleet and all rejected on receipt: "should work" (test it or mark it unverified); summary claims without artifacts; green CI presented as deploy proof when diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 59cff66..5c8a736 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,39 +1,29 @@ -# Version drift is the network's chronic disease — satellites were still -# running a 2.0-era artifact the day 2.3.4 shipped, and nothing about a stale -# host looks broken from the outside. This is the standing fix. +# TEMPLATE FILE: satellites copy this verbatim. # -# TEMPLATE FILE: satellites copy this verbatim. The `dash-network` group is -# the point — a package release lands as ONE reviewable pull request per repo -# instead of five, which is the difference between a rollout and a chore. +# Version updates for pip are OFF BY DESIGN (2026-08-25). On range +# requirements dependabot can only propose FLOOR RAISES, so the old +# `dash-network` allow-list group structurally produced the exact PR class +# the allow-list existed to suppress — 18 of the fleet's 38 open dependabot +# PRs were pip floor-raises. Floors encode minimum-compatibility knowledge +# (a gunicorn floor IS the CVE fact), the resolver already installs latest +# under them, and a raise erases the floor's meaning while adding none. +# Floors move deliberately, through sync specs, every encoding at once; +# drift detection — the group's stated purpose — is already answered on the +# wire by the contract battery (healthz `dash_version` against the floor). +# +# SECURITY updates are unaffected. They ride GitHub's security-update +# channel — dependency graph + Dependabot alerts, enabled per repo in +# Settings → Advanced Security — which needs no entry in this file: +# "There is no interaction between the settings specified in the +# dependabot.yml file and Dependabot security alerts" (GitHub docs, +# code-security/concepts/supply-chain-security/dependabot-security-updates). version: 2 updates: - - package-ecosystem: pip - directory: "/" - schedule: - interval: weekly - day: monday - open-pull-requests-limit: 5 - # Version-update PRs are restricted to the network packages — the drift - # alarm this file exists for. Without this allow-list, dependabot also - # proposes FLOOR-RAISES for every other requirement (gunicorn >=23 → - # >=26, pandas >=1.2.3 → >=3, ...). Those floors encode - # minimum-compatibility knowledge (the gunicorn floor IS the CVE fact), - # the resolver already installs latest under them, and a raise erases - # the floor's meaning while adding none — llms-2plot-dev's fork opened - # five such PRs on its first push, all closed with reasons - # (2026-08-23). SECURITY updates are unaffected: they arrive through - # GitHub's separate security-update channel regardless of this list. - allow: - - dependency-name: "dash*" - - dependency-name: "plotly*" - - dependency-name: "markdown2dash" - groups: - dash-network: - patterns: - - "dash*" - - "plotly*" - - "markdown2dash" - + # Runner/action bumps are mechanical and CI-gated; a human merges the + # group's PR when its CI is green — never a bot actor on main. (The + # 1.6.24 auto-merge workflow was retired in 1.6.25: a GITHUB_TOKEN merge + # gets zero workflow runs on the merge sha, so its deploy is never + # certified by CD.) - package-ecosystem: github-actions directory: "/" schedule: @@ -43,6 +33,8 @@ updates: patterns: - "*" + # Docker base-image bumps stay manual: a Python major is a merits + # decision, never an auto-merge. - package-ecosystem: docker directory: "/" schedule: diff --git a/tests/test_auth_demos.py b/tests/test_auth_demos.py new file mode 100644 index 0000000..6cf4d50 --- /dev/null +++ b/tests/test_auth_demos.py @@ -0,0 +1,51 @@ +"""Every auth-gate demo entry resolves — on THIS site, loudly. + +`build_demo` swallows import failures BY DESIGN (a broken example must never +take down the sign-in funnel), and its warning only fires when that +endpoint's card actually renders — which never happens when the endpoint is +not a page here at all. That combination made a dead entry perfectly silent: +every fork inherited the template's entry, it resolved on none of them, and +their gate cards rendered demo-less from fork time without a line of log +(batch-1 finding, excalidraw, 2026-08-25). This file is the one surface +where a dead entry is loud. + +Byte-verbatim across the fleet: it sweeps THIS repo's DEMOS table against +THIS repo's page registry, so the same bytes hold everywhere. The entries +themselves are site judgment (swap in your own hero example) — an EMPTY +table passes; a dead entry never does. +""" + +from __future__ import annotations + +import importlib + +from lib.auth_demos import DEMOS + + +def test_every_demo_endpoint_is_a_registered_page(app_module): + import dash + + registered = {entry["path"] for entry in dash.page_registry.values()} + dead = sorted(set(DEMOS) - registered) + assert dead == [], ( + f"DEMOS endpoints that are not pages on this site: {dead} — " + "a card that never renders can never surface its own broken demo" + ) + + +def test_every_demo_module_imports_and_exposes_component(app_module): + # import_module is exactly what build_demo does in production; app_module + # first, because the example modules assume the app (and the docs pass + # that imports them at startup) already exists. + problems = [] + for path, spec in sorted(DEMOS.items()): + try: + module = importlib.import_module(spec["module"]) + except Exception as e: + problems.append(f"{path}: {spec['module']} failed to import ({e})") + continue + if not hasattr(module, "component"): + problems.append( + f"{path}: {spec['module']} has no module-level `component`" + ) + assert problems == [], "; ".join(problems) diff --git a/tests/test_claude_kit.py b/tests/test_claude_kit.py index 9dfe7b3..b98da15 100644 --- a/tests/test_claude_kit.py +++ b/tests/test_claude_kit.py @@ -37,13 +37,30 @@ def _ignored(path: str) -> bool: ) +def _in_repo(rel: str) -> bool: + return ".." not in rel and not rel.startswith("/") + + def _machine_fence(kind: str, text: str, where: str) -> None: """The shared pin for machine fences (```yaml sync-verbatim in specs, ```yaml byte-owned in DIVERGENCES.md): exactly one block, `- path` lines with `#` comments, every path repo-relative and real at HEAD. Empty is valid — an empty block is a statement, a missing one is an - omission. `# requires: ` lines (the fan-out's adoption gate, - 1.6.23) are validated like paths — a typo'd gate gates nothing.""" + omission. Gate lines (the fan-out's adoption gates) are validated + like paths — a typo'd gate gates nothing: + + `# requires: ` (1.6.23) — the block applies only where + exists. For paths no pre-existing file can occupy; + where one can, the gate must name a contract instead + (sync/README.md — flows' pre-existing CLAUDE.md, 1.6.28). + `# requires-contract: :: ` (1.6.28) — the block + applies only where exists AND contains . The + clause must be real in THIS repo's copy at HEAD too. + `- # requires: ` (1.6.28) — per-file gate: the + fan-out skips this one copy where is absent, instead + of gating the whole block (clerkhook: a lockdown fork has no + lib/auth_demos.py, legitimately, and must still receive the + rest).""" fences = re.findall( r"^```yaml " + kind + r"[ \t]*\n(.*?)^```[ \t]*$", text, re.M | re.S ) @@ -52,10 +69,34 @@ def _machine_fence(kind: str, text: str, where: str) -> None: f"found {len(fences)}" ) for raw in fences[0].splitlines(): - required = re.match(r"#\s*requires:\s*(.+)$", raw.strip()) + stripped = raw.strip() + if re.match(r"#\s*requires-contract:", stripped): + gate = re.match( + r"#\s*requires-contract:\s*(.+?)\s*::\s*(.+)$", stripped + ) + assert gate, ( + f"{where} {kind}: {raw!r} — `# requires-contract:` takes " + "` :: `; a malformed gate gates nothing" + ) + req, clause = gate.group(1).strip(), gate.group(2).strip() + assert _in_repo(req), ( + f"{where} {kind}: `# requires-contract:` path {req!r} " + "escapes the repo" + ) + assert (REPO / req).is_file(), ( + f"{where} {kind}: `# requires-contract:` names {req!r} " + "which does not exist at HEAD — a typo'd gate gates nothing" + ) + assert clause in (REPO / req).read_text(), ( + f"{where} {kind}: `# requires-contract:` clause {clause!r} " + f"is not in this repo's own {req} — a typo'd clause gates " + "nothing" + ) + continue + required = re.match(r"#\s*requires:\s*(.+)$", stripped) if required: req = required.group(1).strip() - assert ".." not in req and not req.startswith("/"), ( + assert _in_repo(req), ( f"{where} {kind}: `# requires:` path {req!r} escapes the repo" ) assert (REPO / req).is_file(), ( @@ -63,20 +104,36 @@ def _machine_fence(kind: str, text: str, where: str) -> None: "not exist at HEAD — a typo'd gate gates nothing" ) continue - entry = raw.split("#", 1)[0].strip() + entry, _, comment = raw.partition("#") + entry = entry.strip() if not entry: continue assert entry.startswith("- "), ( f"{where} {kind}: {raw!r} is not a `- path` line" ) path = entry[2:].strip() - assert ".." not in path and not path.startswith("/"), ( + assert _in_repo(path), ( f"{where} {kind}: {path!r} escapes the repo" ) assert (REPO / path).is_file(), ( f"{where} {kind}: {path!r} does not exist at HEAD " "— the machine would act on nothing or the wrong thing" ) + # A per-file gate is the WHOLE trailing comment, `requires: ` + # from its first character; prose comments that merely mention the + # word stay prose. + per_file = re.match(r"\s*requires:\s*(.+)$", comment) + if per_file: + gate_path = per_file.group(1).strip() + assert _in_repo(gate_path), ( + f"{where} {kind}: per-file gate on {path!r} escapes the " + f"repo: {gate_path!r}" + ) + assert (REPO / gate_path).is_file(), ( + f"{where} {kind}: per-file gate on {path!r} names " + f"{gate_path!r} which does not exist at HEAD — a typo'd " + "gate gates nothing" + ) def test_kit_files_exist_and_are_not_ignored():