Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

fabric drift: unregistered is computed over a watch set nothing reconciles against the cards the fabric already holds

Abierto
#73 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
42/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python, shell
Área
cli, tooling

Línea de trabajo

Start with .agentic-framework/agents/fabric/lib/drift.sh and the new watchset_coverage.py; trace how drift expands watch patterns and discovers registered card locations. Then run the consumer-side fabricDriftWatchsetCoverage.test.js cases and inspect executed output, including the malformed-pattern control. Done means drift reports uncovered card locations and UNKNOWN on reader failure, with the summary updated and existing behavior preserved.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Context

  • Framework version: 1.6.258
  • Project: 050-email-archive (shared-tooling mode)
  • Reported: 2026-09-25T09:27:16Z

Description

Problem

fw fabric drift section 1 computes:

unregistered = expand_patterns(watch-patterns.yaml) - card_locations

Both terms are bounded by the watch set, so the check can only ever produce a finding about a
file the glob already reaches. Nothing asks the converse — whether the watch set still reaches
everywhere the fabric already holds cards.

Where it does not, unregistered: 0 stops being evidence about the tree and becomes evidence
about the glob, and the report cannot tell those apart.

Measured (Pen / email-archive, 2026-09-25)

76 of 724 on-disk card locations (10.5%) lie outside the watch set, across 39 directories:

kind count note
.sh 27 no pattern covers scripts shell scripts at all
.css 27 cards exist although a recorded decision says CSS gets no cards
.js 9 incl. src/config/, scripts/lib/, scripts/tests/
other 13 .service, .timer, .ps1, .html, .py, extensionless, and one dist/ build artifact the patterns explicitly exclude

Demonstrated, not argued

src/config/ holds three files. Two have cards — so the project had already decided the
directory is in-fabric. The third, src/config/paths.js, has 38 importers and no card, and
no pattern covers src/config. Adding a recursive src/config JS glob:

unwatched    76 -> 74
unregistered  0 -> 1   naming src/config/paths.js

The 0 was false. Only the guard's input disagreed with the cards.

This is the same defect class as #70 (drift blind to card-vs-source divergence): the check reads
the wrong input and reports a confident 0.

Second, separate defect found while negative-controlling the fix

With a malformed watch-patterns.yaml, expand_patterns.py exits 2. Section 1 builds its
set-difference as comm -23 <(python3 expand_patterns.py ... 2>/dev/null | sort -u) <(registered):
stderr is discarded and a process substitution's exit status is invisible to comm, so a crashed
expander contributes an empty left-hand set and comm reports nothing missing.

Result: unregistered: 0 on a crashed reader. The new section, reading the same broken input,
reports UNKNOWN. Two sections of one report disagreeing about whether the input was readable.

This is the failure mode #70's own section-4 comment says "must not be reachable from here" — and
it is reachable from section 1, which predates that reasoning. Not fixed in this patch (one
structural change at a time); the fix is to capture the expander's exit status the way section 4
captures _div_rc. The orphaned/stale sections were not audited for the same hazard.

The patch

Adds a fifth section, Watch-set coverage, naming every directory where the fabric holds cards
the watch set never scans, and puts unwatched: N on the Summary line beside unregistered: N
so the 0 cannot be read alone.

Design notes:

  • Reuses expand_patterns.py rather than reimplementing glob logic — the reason T-1842
    centralised it: a second reader would be free to disagree with the section it reports on.
  • Fails loudly. Exits 2 when it cannot read its input; drift prints UNKNOWN, never 0.
  • Not a policy check. It does not decide which files deserve cards, and exit status is
    untouched
    . Severity is left informational deliberately: the WARN-vs-ratchet question is still
    open for section 4 and one change should not settle it for two sections at once.
  • A card whose file is absent from disk is not counted here — that is section 2 (orphaned).
    Counting it would report one defect twice.

Negative controls (all observed)

control result
A — drop a watched glob (src/database) section 5 names src/database{,/migrations,/models} (77 cards); unregistered stays 0, noticing nothing
B — add the src/config glob unwatched 76→74, unregistered 0→1 naming paths.js
C — corrupt watch-patterns.yaml expander exits 2; section 5 UNKNOWN, section 1 still 0

Consumer-side test (fabricDriftWatchsetCoverage.test.js, 9 cases) asserts on executed output,
never on a grep of drift.sh — a source grep matches the comment explaining a fix as readily as
the fix. Proven non-vacuous: reverting drift.sh turns the 3 repo-shape tests red and correctly
leaves the 5 hermetic ones green.

Diff

diff --git a/.agentic-framework/agents/fabric/lib/drift.sh b/.agentic-framework/agents/fabric/lib/drift.sh
index 60dda0f1..b7721dd1 100644
--- a/.agentic-framework/agents/fabric/lib/drift.sh
+++ b/.agentic-framework/agents/fabric/lib/drift.sh
@@ -254,7 +254,66 @@ $_div_unres detected import(s) whose target has no card)"
     fi
 
     echo ""
-    echo -e "${BOLD}Summary:${NC} unregistered: $unregistered, orphaned: $orphaned, stale: $stale, divergent: $divergent ($_div_edges edge(s))"
+
+    # 5. Watch-set coverage — does the watch set still reach where the fabric has cards?
+    # T-2253 / G-FABRIC-WATCHSET-BLIND-TO-OWN-CARDS.
+    #
+    # Section 1 computes `unregistered = watched_files - card_locations`. Both terms are
+    # bounded by the watch set, so it can only ever find a file the glob already reaches.
+    # Nothing asked the converse. When the watch set does not cover a directory the fabric
+    # already holds cards for, `unregistered: 0` stops being evidence about the tree and
+    # becomes evidence about the glob — and the output cannot tell those apart.
+    #
+    # Measured here on 2026-09-25: 76 of 724 on-disk card locations (10.5%) across 39
+    # directories, including 27 registered shell scripts (`scripts/*.sh` has no pattern at
+    # all) and `src/config/`, where two of three files have cards and the third —
+    # `src/config/paths.js`, 38 importers — has none and could never be flagged.
+    #
+    # Deliberately NOT a policy check: it does not decide which files deserve cards. It
+    # reports only where the fabric's own cards prove a directory is in scope while the
+    # watch set disagrees. Severity is left informational on purpose — exit status is
+    # untouched — because the WARN-vs-ratchet question is still open for section 4 and one
+    # run should not settle it for two sections at once.
+    echo -e "${CYAN}Watch-set coverage (cards vs watch set):${NC}"
+    local unwatched=0 _wc_raw _wc_rc=0 _wc_dirs=0
+    # `|| _wc_rc=$?` for the reason section 4 documents: under `set -e` a failing command
+    # substitution aborts the function, and a detector that cannot run must never be
+    # indistinguishable from one that found nothing.
+    _wc_raw=$(python3 "$LIB_DIR/watchset_coverage.py" "$watch_file" "$PROJECT_ROOT" \
+        "$COMPONENTS_DIR" 2>/dev/null) || _wc_rc=$?
+    if [ "$_wc_rc" -ne 0 ] || ! grep -q '^##UNWATCHED_LOCATIONS=' <<< "$_wc_raw"; then
+        echo "  ? UNKNOWN — the coverage reader did not run (watchset_coverage.py exited" \
+             "$_wc_rc). This is NOT 'fully covered'."
+        unwatched="UNKNOWN"
+    else
+        unwatched=$(sed -n 's/^##UNWATCHED_LOCATIONS=\([0-9]*\)##$/\1/p' <<< "$_wc_raw")
+        _wc_dirs=$(sed -n 's/^##UNWATCHED_DIRS=\([0-9]*\)##$/\1/p' <<< "$_wc_raw")
+        : "${unwatched:=0}" "${_wc_dirs:=0}"
+        local _wc_shown=0
+        while IFS= read -r _wc_line; do
+            case "$_wc_line" in
+                '  ! '*) ;;
+                *) continue ;;
+            esac
+            if [ "$_wc_shown" -lt 25 ]; then
+                echo "$_wc_line"
+                _wc_shown=$((_wc_shown + 1))
+            fi
+        done <<< "$_wc_raw"
+        if [ "$_wc_dirs" -gt "$_wc_shown" ]; then
+            echo "  … $((_wc_dirs - _wc_shown)) more director(ies) (full list:" \
+                 "python3 $LIB_DIR/watchset_coverage.py $watch_file $PROJECT_ROOT $COMPONENTS_DIR)"
+        fi
+        if [ "$unwatched" -eq 0 ]; then
+            echo "  (none)"
+        else
+            echo "  → $unwatched card location(s) in $_wc_dirs director(ies) the watch set" \
+                 "never scans; section 1 cannot report on anything there."
+        fi
+    fi
+
+    echo ""
+    echo -e "${BOLD}Summary:${NC} unregistered: $unregistered, orphaned: $orphaned, stale: $stale, divergent: $divergent ($_div_edges edge(s)), unwatched: $unwatched"
 
     if [ "$summary_flag" = "--summary" ]; then
         echo "unregistered: $unregistered"
@@ -262,6 +321,8 @@ $_div_unres detected import(s) whose target has no card)"
         echo "stale: $stale"
         echo "divergent: $divergent"
         echo "divergent_edges: $_div_edges"
+        echo "unwatched: $unwatched"
+        echo "unwatched_dirs: $_wc_dirs"
     fi
 
     return 0
diff --git a/.agentic-framework/agents/fabric/lib/watchset_coverage.py b/.agentic-framework/agents/fabric/lib/watchset_coverage.py
new file mode 100644
index 00000000..0cc74eaa
--- /dev/null
+++ b/.agentic-framework/agents/fabric/lib/watchset_coverage.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# Fabric — watch-set coverage of the fabric's own registered surface (T-2253, Pen).
+#
+# WHAT THIS ANSWERS, AND WHY NOTHING ELSE DID
+#
+# drift.sh section 1 computes:
+#
+#     unregistered = expand_patterns(watch-patterns.yaml) - card_locations
+#
+# i.e. "is there a watched file with no card". Every term on the right is bounded by the
+# watch set, so the check can only ever produce a finding about a file the watch set
+# already reaches. Nothing asks the converse — whether the watch set still reaches
+# everywhere the fabric ALREADY HOLDS CARDS. When it does not, `unregistered: 0` is not
+# evidence of a registered tree; it is evidence of a narrow glob, and the two are
+# indistinguishable in the output.
+#
+# Measured in Pen on 2026-09-25: 76 of 724 on-disk card locations (10.5%) lay outside the
+# watch set across 39 directories. Worked example — `src/config/` holds three files; two
+# have cards (so the project already decided the directory is in-fabric) and the third,
+# `src/config/paths.js` with 38 importers, has none. No pattern covers `src/config/**`, so
+# section 1 could never flag it, and reported 0. It surfaced only because T-2250's
+# divergence detector — a different reader — counted it among 37 anonymous "detected
+# import(s) whose target has no card".
+#
+# This is deliberately NOT a policy check. It does not decide which files deserve cards.
+# It reports only where the fabric's own cards prove a directory is in scope while the
+# watch set does not cover it, so the two inputs can no longer silently disagree.
+#
+# WHY IT SHELLS OUT TO expand_patterns.py
+#
+# Same reason T-1842 centralised that expansion in the first place: a second, independent
+# reimplementation of "what is watched" would be free to disagree with section 1, and a
+# coverage report that disagrees with the check it is reporting on is worse than none.
+# One reader, one answer. If that reader fails, this script fails loudly (exit 2) so the
+# caller can print UNKNOWN — a coverage checker that cannot read its input must never be
+# mistaken for one that found full coverage.
+#
+# Usage:
+#   python3 watchset_coverage.py <watch-patterns.yaml> <project_root> <components_dir>
+#
+# Output: one "  ! <dir>  (<n> card(s))" line per uncovered directory, then sentinels
+#   ##UNWATCHED_LOCATIONS=<n>##
+#   ##UNWATCHED_DIRS=<n>##
+# Exit codes: 0 ok, 2 pattern expansion failed, 3 missing argv / unreadable components dir.
+
+from __future__ import annotations
+
+import collections
+import os
+import subprocess
+import sys
+
+
+def main() -> int:
+    if len(sys.argv) < 4:
+        sys.stderr.write(
+            "usage: watchset_coverage.py <watch-patterns.yaml> <project_root> "
+            "<components_dir>\n"
+        )
+        return 3
+
+    watch_file, project_root, components_dir = sys.argv[1], sys.argv[2], sys.argv[3]
+    lib_dir = os.path.dirname(os.path.abspath(__file__))
+
+    if not os.path.isdir(components_dir):
+        sys.stderr.write(f"watchset_coverage.py: no components dir: {components_dir}\n")
+        return 3
+
+    # Single source of truth for "what is watched" — see header.
+    try:
+        proc = subprocess.run(
+            [sys.executable, os.path.join(lib_dir, "expand_patterns.py"),
+             watch_file, project_root],
+            capture_output=True, text=True, timeout=120,
+        )
+    except (OSError, subprocess.SubprocessError) as exc:
+        sys.stderr.write(f"watchset_coverage.py: could not run expand_patterns.py: {exc}\n")
+        return 2
+    if proc.returncode != 0:
+        sys.stderr.write(
+            f"watchset_coverage.py: expand_patterns.py exited {proc.returncode}\n"
+        )
+        return 2
+
+    watched = {line.strip() for line in proc.stdout.splitlines() if line.strip()}
+
+    # Card locations. Read with the same `^location:` convention drift.sh uses so a card
+    # this script counts is a card that check counts.
+    locations = []
+    try:
+        for fn in sorted(os.listdir(components_dir)):
+            if not fn.endswith(".yaml"):
+                continue
+            try:
+                with open(os.path.join(components_dir, fn), "r", errors="replace") as fh:
+                    for line in fh:
+                        if line.startswith("location:"):
+                            loc = line.split("location:", 1)[1].strip()
+                            if loc:
+                                locations.append(loc)
+                            break
+            except OSError:
+                continue
+    except OSError as exc:
+        sys.stderr.write(f"watchset_coverage.py: cannot list {components_dir}: {exc}\n")
+        return 3
+
+    # A card whose file is absent from disk is drift's section 2 (orphaned), not ours.
+    # Counting it here would double-report one defect as two, the exact error T-2112 made
+    # on the Qdrant join key.
+    uncovered = collections.Counter()
+    total = 0
+    for loc in set(locations):
+        if loc in watched:
+            continue
+        if not os.path.isfile(os.path.join(project_root, loc)):
+            continue
+        total += 1
+        uncovered[os.path.dirname(loc) or "."] += 1
+
+    for directory, count in sorted(uncovered.items()):
+        print(f"  ! {directory}  ({count} card location(s) not covered)")
+
+    print(f"##UNWATCHED_LOCATIONS={total}##")
+    print(f"##UNWATCHED_DIRS={len(uncovered)}##")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

Reported via fw upstream report from field installation

Lenguaje dominante
Shell
Estrellas
16
Forks
6
Merge medio
6 h 24 min
PR fusionados (30 d)
5

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de DimitriGeelen/agentic-engineering-framework

Todos los issues de DimitriGeelen/agentic-engineering-framework

Issues similares

Más issues de Shell/Bash

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.