Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đang mở
#73 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
42/100
Loại issue
Tính năng
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python, shell
Lĩnh vực
cli, tooling

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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

Ngôn ngữ chính
Shell
Star
16
Fork
6
Merge trung bình
6 giờ 24 phút
Pull request đã merge (30 ngày)
5

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của DimitriGeelen/agentic-engineering-framework

Tất cả issue của DimitriGeelen/agentic-engineering-framework

Issue tương tự

Thêm issue về Shell/Bash

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.