@ccoalm/ccl-skills 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +34 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_abort_leak_state_helpers.sh +148 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +7 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate_abort_leak.sh +271 -34
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +12 -12
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +7 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +3 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +7 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +47 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +28 -29
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +165 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +23 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +303 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +222 -91
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +85 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +123 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +6 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +12 -12
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +455 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +20 -20
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_liveness_predicate_gate.sh +288 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +4 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/deliverable-doc-genre-skeletons.md +133 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +318 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/AGENTS.md +46 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/doc-lint.py +246 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/figure-lint.py +1092 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/mutation_probe.sh +100 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/test_figure_and_doc_lint.sh +375 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/control.md +10 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/empty-header.md +6 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fake-header.md +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fenced-noise.md +14 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-dangling.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan-captioned.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig.png +0 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/imbalance.md +41 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/no-unit.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/should-be-chart.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/tables-only-clean.md +35 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/unfilled.md +7 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/wide-table.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/bad-viewbox.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/blackmarker.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/control.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/crossings.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/cvd-confusable.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/decorative-line.svg +10 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-no-arrow.svg +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-vague.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-contract.json +21 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-is-a-list.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/flow-mixed.svg +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/low-contrast.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/malformed.svg +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-aria.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-group.svg +10 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-legend.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-title.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-viewbox.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/offcontract-shape.svg +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/overflow.svg +13 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/transformed.svg +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/ungrouped-card.svg +12 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/unlabeled-edge.svg +13 -0
- package/dist/assets/release.json +276 -31
- package/package.json +1 -1
|
@@ -270,7 +270,18 @@ else
|
|
|
270
270
|
end
|
|
271
271
|
' "$root"
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
# Public DOMAIN word list: a weak-proxy FALLBACK, not the authoritative R0 gate.
|
|
274
|
+
# The real invariant — "this public text identifies one specific organization or
|
|
275
|
+
# project" — is not decidable by a public regex, which is why the private alias
|
|
276
|
+
# audit (ALIAS_AUDIT_CMD -> r0_status=private-ok) is authoritative and this list
|
|
277
|
+
# only covers environments without it. Consequence, stated rather than fought:
|
|
278
|
+
# these terms are ANOTHER domain's ordinary vocabulary, so they decay — a term
|
|
279
|
+
# outlives the project it proxied for and then only produces false positives.
|
|
280
|
+
# Maintain by RETIREMENT, not by growth: when a term no longer proxies anything
|
|
281
|
+
# live, delete it (quantify current hits first, and keep a retained-term control
|
|
282
|
+
# so the scan is proven still able to fail). Do not add generic technical words.
|
|
283
|
+
# Both directions are pinned by test_entrypoint_domain_scan_terms.sh.
|
|
284
|
+
if leak_output="$(rg -n 'code\.[[:alnum:].-]+|figma\.com/files|[0-9]{8,}|\x{6559}\x{5e08}|\x{5b66}\x{751f}|\x{8003}\x{8bd5}|\x{5b66}\x{6821}|\x{9605}\x{5377}|\x{51fa}\x{5377}|\x{5b66}\x{60c5}' "$root"/skills/*/SKILL.md "$root"/skills/*/references/*.md 2>/dev/null)"; then
|
|
274
285
|
echo "$leak_output"
|
|
275
286
|
echo "entrypoint_or_reference_domain_scan_failed" >&2
|
|
276
287
|
exit 1
|
|
@@ -514,6 +525,159 @@ if [[ -n "${gid_hits//[$'\n']/}" ]]; then
|
|
|
514
525
|
fi
|
|
515
526
|
echo "git_identity_predicate_scan_ok"
|
|
516
527
|
|
|
528
|
+
# Anti-pattern 28 — process liveness decided by an EXISTENCE test that a corpse answers
|
|
529
|
+
# (see references/recurring-anti-patterns-checklist.md). A pid whose process has exited
|
|
530
|
+
# but has not been reaped is still in the process table: it answers `kill -0`, and its
|
|
531
|
+
# ppid still reads — as 1 once it is reparented. A check that concludes "alive" or
|
|
532
|
+
# "orphaned and alive" from existence alone therefore says yes about a corpse, while the
|
|
533
|
+
# verdict scans in the same suite exclude zombies and say gone — one process state, two
|
|
534
|
+
# answers, and a probe that reds while the code under test is behaving.
|
|
535
|
+
# Promoted from checklist to gate because the class recurred three times in CI on the
|
|
536
|
+
# abort-leak probe, each time on a different assertion, each time asserting the probe's
|
|
537
|
+
# environment rather than the suite.
|
|
538
|
+
# Scope: `test_*.sh` / `test.sh` under the repo — the surface where such a predicate is a
|
|
539
|
+
# TEST VERDICT rather than a signalling guard. Excluded on purpose: non-test shell (a
|
|
540
|
+
# `kill -0` before signalling asks about existence, which is the right question there),
|
|
541
|
+
# and WHOLE-LINE comments (this comment block names the banned spelling).
|
|
542
|
+
# The predicate is the orphan-oracle shape specifically: a `ps -o ppid=` read compared
|
|
543
|
+
# against init's pid on the same line. That shape has exactly one meaning — "has this
|
|
544
|
+
# been reparented, i.e. is it a live orphan" — so precision is high; a ppid read used to
|
|
545
|
+
# IDENTIFY a parent (the common use) is untouched. A process-state consult (`stat=` or a
|
|
546
|
+
# helper whose name ends in `_state`) within the window clears the line, which is the fix.
|
|
547
|
+
# Recall limits — stated so the contract is not overclaimed, per the checklist's
|
|
548
|
+
# promotion guidance (high precision plus a documented recall limit beats broad matching
|
|
549
|
+
# for a BLOCKING gate):
|
|
550
|
+
# 1. `while kill -0 "$pid"` watchdog loops are NOT flagged. Two exist here; both wait on
|
|
551
|
+
# a direct child and `wait` for it immediately after, so the shell reaps it and the
|
|
552
|
+
# loop ends — a narrower risk than the shape above, and forcing churn on them would
|
|
553
|
+
# trade a real false-positive cost for an unproven gain.
|
|
554
|
+
# 2. a liveness branch spelled with a bare `kill -0` inside a loop BODY.
|
|
555
|
+
# 3. any dynamic or indirect spelling.
|
|
556
|
+
# Those stay checklist plus adversarial-challenge checks.
|
|
557
|
+
liveness_hits=""
|
|
558
|
+
liveness_list=$(mktemp "${TMPDIR:-/tmp}/ccl-skills-liveness.XXXXXX") || {
|
|
559
|
+
echo "liveness_predicate_scan_error: mktemp failed" >&2; exit 1; }
|
|
560
|
+
# Guarded rather than the `${var:-/dev/null}` idiom: this trap is installed
|
|
561
|
+
# unconditionally, so an unset var would make the cleanup `rm -f /dev/null` — a no-op for
|
|
562
|
+
# an unprivileged user and a deleted device node for a root container. Never name a path
|
|
563
|
+
# in a removal that a fallback can turn into someone else's file.
|
|
564
|
+
liveness_scan_cleanup() {
|
|
565
|
+
[ -n "${liveness_list:-}" ] && [ -e "${liveness_list:-}" ] && rm -f "$liveness_list"
|
|
566
|
+
[ -n "${gid_list:-}" ] && [ -e "${gid_list:-}" ] && rm -f "$gid_list"
|
|
567
|
+
return 0
|
|
568
|
+
}
|
|
569
|
+
trap liveness_scan_cleanup EXIT
|
|
570
|
+
# Same traversal hardening as Anti-pattern 27, and for the same reason: every leg here
|
|
571
|
+
# exists because its absence makes the gate print "ok" for a scan that did not happen.
|
|
572
|
+
if find -L "$root" -name '*.sh' -type f -print0 > "$liveness_list" 2>/dev/null; then
|
|
573
|
+
liveness_find_rc=0
|
|
574
|
+
else
|
|
575
|
+
liveness_find_rc=$?
|
|
576
|
+
fi
|
|
577
|
+
if [[ "$liveness_find_rc" -ne 0 ]]; then
|
|
578
|
+
rm -f "$liveness_list"
|
|
579
|
+
echo "liveness_predicate_scan_error: find exited $liveness_find_rc — the walk is INCOMPLETE, which is not the same as clean" >&2
|
|
580
|
+
exit 1
|
|
581
|
+
fi
|
|
582
|
+
while IFS= read -r -d '' liveness_file; do
|
|
583
|
+
[[ -n "$liveness_file" ]] || continue
|
|
584
|
+
case "${liveness_file##*/}" in test_*.sh|test.sh) : ;; *) continue ;; esac
|
|
585
|
+
# `[^=]*[^!=]=` rather than `.*=`: an unrestricted `.*` swallows the `!` of a `!=`, so
|
|
586
|
+
# the negated assertion `[ "$(ps -o ppid= -p "$pid")" != "1" ]` — which checks a process
|
|
587
|
+
# is NOT reparented, the opposite of the banned oracle — was reported as a violation.
|
|
588
|
+
# A false positive on a blocking gate is the failure this checklist's promotion
|
|
589
|
+
# guidance weighs heaviest, because it is what gets a gate loosened until it catches
|
|
590
|
+
# nothing.
|
|
591
|
+
if liveness_out="$(grep -nE 'ps[[:space:]]+-o[[:space:]]+ppid=[^=]*[^!=]=[[:space:]]*"?1"?[[:space:]]*\]' "$liveness_file" 2>/dev/null)"; then
|
|
592
|
+
liveness_rc=0
|
|
593
|
+
else
|
|
594
|
+
liveness_rc=$?
|
|
595
|
+
fi
|
|
596
|
+
if [[ "$liveness_rc" -gt 1 ]]; then
|
|
597
|
+
rm -f "$liveness_list"
|
|
598
|
+
echo "liveness_predicate_scan_error: grep exited $liveness_rc while scanning ${liveness_file#"$root"/} — unreadable or I/O failure means the result is UNKNOWN, not clean" >&2
|
|
599
|
+
exit 1
|
|
600
|
+
fi
|
|
601
|
+
[[ "$liveness_rc" -eq 0 ]] || continue
|
|
602
|
+
while IFS= read -r liveness_line; do
|
|
603
|
+
[[ -n "$liveness_line" ]] || continue
|
|
604
|
+
liveness_no="${liveness_line%%:*}"
|
|
605
|
+
liveness_text="${liveness_line#*:}"
|
|
606
|
+
[[ "$liveness_text" =~ ^[[:space:]]*# ]] && continue
|
|
607
|
+
# Window clear: a process-state consult within two lines either side is the fix, so
|
|
608
|
+
# a corrected site stops being reported without needing a waiver marker.
|
|
609
|
+
liveness_lo=$(( liveness_no > 2 ? liveness_no - 2 : 1 ))
|
|
610
|
+
liveness_hi=$(( liveness_no + 2 ))
|
|
611
|
+
# Whole-line comments are dropped before the window is inspected: a prose mention
|
|
612
|
+
# such as a TODO naming the helper would otherwise clear a real violation beside it,
|
|
613
|
+
# and the gate would print ok for a line it had actually found. The waiver must be
|
|
614
|
+
# CODE that consults process state, not a note saying someone should.
|
|
615
|
+
# Only WHOLE-LINE comments are dropped, matching how the hit line itself is filtered.
|
|
616
|
+
# Stripping trailing comments would need to decide whether a `#` opens a comment or
|
|
617
|
+
# belongs to `${var#prefix}`, which needs a shell parser — the same call Anti-pattern
|
|
618
|
+
# 27 makes above. Recall limit: a mention in a TRAILING comment can still clear the
|
|
619
|
+
# window; the checklist row records it.
|
|
620
|
+
liveness_win="$(sed -n "${liveness_lo},${liveness_hi}p" "$liveness_file" 2>/dev/null |
|
|
621
|
+
grep -vE '^[[:space:]]*#' || true)"
|
|
622
|
+
# A direct `ps -o stat=` in the window is the consult itself. Anchored on `-o stat=`
|
|
623
|
+
# rather than bare `stat=`: an unrelated assignment such as `stat=unknown` is not a
|
|
624
|
+
# process-state read, and accepting it was the fourth way this waiver was found to
|
|
625
|
+
# clear a real hit.
|
|
626
|
+
if printf '%s' "$liveness_win" | grep -qE '\-o[[:space:]]*stat='; then
|
|
627
|
+
continue
|
|
628
|
+
fi
|
|
629
|
+
# Otherwise the window may CALL a state helper — but only one this file actually
|
|
630
|
+
# defines and which itself consults process state. Accepting any `*_state` token
|
|
631
|
+
# made an unrelated `record_state "$pid"` read as remediation and skipped a real
|
|
632
|
+
# hit, which is a hole rather than a stated recall limit.
|
|
633
|
+
# The file must also actually consult process state somewhere, so a file with no
|
|
634
|
+
# `stat=` at all cannot be cleared by a bookkeeping helper that merely ends in
|
|
635
|
+
# `_state`. Recall limit: a file that separately defines such a helper AND reads
|
|
636
|
+
# process state elsewhere clears the window without proving the two are connected —
|
|
637
|
+
# tying a call to its definition needs a shell parser, the same call made above.
|
|
638
|
+
# Deliberately POSIX-portable: `\b` and BSD-sed `\?` are not, and a silently failing
|
|
639
|
+
# match here would loosen the gate on exactly the host that runs it most.
|
|
640
|
+
# The named helper's OWN BODY must reach a process-state read. Checking "the file
|
|
641
|
+
# defines it" and "the file reads state somewhere" as two independent facts let a
|
|
642
|
+
# hollow helper plus an unrelated `ps -o stat=` elsewhere clear a real hit — the two
|
|
643
|
+
# conditions were never tied to each other. awk rather than sed: BSD sed lacks `\?`
|
|
644
|
+
# and a silently failing match here loosens the gate on the host that runs it most.
|
|
645
|
+
liveness_helper_ok=0
|
|
646
|
+
while IFS= read -r liveness_helper; do
|
|
647
|
+
[[ -n "$liveness_helper" ]] || continue
|
|
648
|
+
if awk -v fn="$liveness_helper" '
|
|
649
|
+
$0 ~ "^[[:space:]]*(function[[:space:]]+)?" fn "[[:space:]]*\\(\\)" {
|
|
650
|
+
# The declaration line is part of the body: a one-line definition both opens
|
|
651
|
+
# and closes here. Skipping it with a bare `next` left `inbody` set through
|
|
652
|
+
# `fn() { echo live; }` and credited the NEXT function\047s state read to this
|
|
653
|
+
# hollow one.
|
|
654
|
+
if ($0 ~ /-o[[:space:]]*stat=/) { found = 1; exit }
|
|
655
|
+
if ($0 ~ /}/) { exit }
|
|
656
|
+
inbody = 1
|
|
657
|
+
next
|
|
658
|
+
}
|
|
659
|
+
inbody && /-o[[:space:]]*stat=/ { found = 1; exit }
|
|
660
|
+
inbody && /^[[:space:]]*}/ { exit }
|
|
661
|
+
END { exit !found }
|
|
662
|
+
' "$liveness_file" 2>/dev/null; then
|
|
663
|
+
liveness_helper_ok=1
|
|
664
|
+
break
|
|
665
|
+
fi
|
|
666
|
+
done < <(printf '%s\n' "$liveness_win" | grep -oE '[A-Za-z_][A-Za-z0-9_]*_state' | sort -u)
|
|
667
|
+
[[ "$liveness_helper_ok" -eq 1 ]] && continue
|
|
668
|
+
liveness_hits+="${liveness_file#"$root"/}:$liveness_line"$'\n'
|
|
669
|
+
done <<< "$liveness_out"
|
|
670
|
+
done < "$liveness_list"
|
|
671
|
+
rm -f "$liveness_list"
|
|
672
|
+
if [[ -n "${liveness_hits//[$'\n']/}" ]]; then
|
|
673
|
+
# Trailing newline restored explicitly: command substitution strips it, which ran the
|
|
674
|
+
# last hit and the diagnosis together on one line.
|
|
675
|
+
printf '%s\n' "$(printf '%s' "$liveness_hits" | sort)"
|
|
676
|
+
echo "liveness_predicate_scan_failed: the lines above decide that a process is a LIVE orphan from a ppid read alone, which an unreaped corpse answers the same way — the check says alive while the suite's own verdict scans exclude zombies and say gone. Consult process STATE (\`ps -o stat=\`, or a helper that returns live/zombie/absent) before concluding liveness. See skill-extraction-workflow/references/recurring-anti-patterns-checklist.md (Anti-pattern 28)." >&2
|
|
677
|
+
exit 1
|
|
678
|
+
fi
|
|
679
|
+
echo "liveness_predicate_scan_ok"
|
|
680
|
+
|
|
517
681
|
# Evidence-card leak PREFLIGHT (best-effort, NOT the gate of record): catches obvious
|
|
518
682
|
# card-shaped markdown accidentally committed under skills/** (L0 storage rule, see
|
|
519
683
|
# l0-l1-l2-routing.md). Delegated to a dedicated, self-tested detector. The gate runs the
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# F4
|
|
4
|
+
# F4 signal dashboard — advisory display roll-up (0-10) + same-stick change.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Shows the F4 dimensions together and retains a weighted 0-10 display value plus
|
|
7
|
+
# a same-corpus/dimensions delta for compatibility with existing reports. The
|
|
8
|
+
# shape resembles OpenSSF Scorecard, but the value is not a verdict that the
|
|
9
|
+
# skill repository is globally better or worse: these dimensions have different
|
|
10
|
+
# semantics, and a quality or safety failure is never averaged away.
|
|
11
11
|
#
|
|
12
12
|
# Dimensions (each scored 0-10, risk-weighted OpenSSF-style):
|
|
13
13
|
# structural weight 10 (Critical) — validate-skill.sh: skills load / no leakage
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
# (structural validation + Tier-1 blocking findings) remain the source of truth
|
|
30
30
|
# and block independently of this score. Per Goodhart's law ("when a measure
|
|
31
31
|
# becomes a target, it ceases to be a good measure"), a composite that became a
|
|
32
|
-
# merge gate would just get gamed; it stays a lens, not a gate
|
|
32
|
+
# merge gate would just get gamed; it stays a navigation lens, not a gate or an
|
|
33
|
+
# acceptance grade. The history file
|
|
33
34
|
# is git-ignored for the same reason — a committed number invites tuning the
|
|
34
35
|
# number instead of the repo.
|
|
35
36
|
#
|
|
@@ -289,7 +290,14 @@ unless no_write
|
|
|
289
290
|
end
|
|
290
291
|
|
|
291
292
|
report = entry.merge(
|
|
293
|
+
# `band` keeps its name and values for existing report consumers, but the
|
|
294
|
+
# machine surface must not be the one place the withdrawal does not reach: a
|
|
295
|
+
# consumer reads this JSON, not the header line, and CLEAN / WARNING / NEEDS
|
|
296
|
+
# WORK / CRITICAL is grade vocabulary that gets quoted as an acceptance verdict.
|
|
297
|
+
# The qualifier therefore travels as its own field rather than only in `puts`.
|
|
292
298
|
"band" => band(composite),
|
|
299
|
+
"band_semantics" => "navigation-only display label; not an acceptance grade or an overall-quality verdict",
|
|
300
|
+
"score_semantics" => "weighted display roll-up over the dims present in this run; comparable only against the same corpus and dims",
|
|
293
301
|
"weights_present" => present.to_h { |k, _| [k.to_s, RISK[k]] },
|
|
294
302
|
"details" => dims.to_h { |k, d| [k.to_s, d[:detail]] },
|
|
295
303
|
"trend" => trend,
|
|
@@ -306,13 +314,18 @@ if out_json
|
|
|
306
314
|
end
|
|
307
315
|
|
|
308
316
|
unless quiet
|
|
309
|
-
puts "F4
|
|
317
|
+
puts "F4 SIGNAL DASHBOARD (advisory — not a gate or overall-quality verdict)"
|
|
310
318
|
puts " repo=#{repo_sha} corpus=#{corpus} dims=#{present_dims.join(',')}"
|
|
311
319
|
dims.each do |k, d|
|
|
312
320
|
s = d[:score].nil? ? " - " : format("%2d/10", d[:score])
|
|
313
321
|
puts " #{k.to_s.ljust(15)} #{s} #{d[:detail]}"
|
|
314
322
|
end
|
|
315
|
-
|
|
323
|
+
# The band label carries grade vocabulary (CLEAN / WARNING / NEEDS WORK /
|
|
324
|
+
# CRITICAL) that reads as an acceptance verdict and gets quoted downstream as
|
|
325
|
+
# one. The header withdrew that claim; printing the bare label next to the
|
|
326
|
+
# number would have left the executable surface contradicting the withdrawal,
|
|
327
|
+
# so the qualifier travels with the label rather than only with the header.
|
|
328
|
+
puts " DISPLAY ROLL-UP #{format('%.1f', composite)}/10 #{band(composite)} (navigation only, not an acceptance grade)"
|
|
316
329
|
if prior_comparable
|
|
317
330
|
arrow = trend.positive? ? "IMPROVING +#{trend}" : (trend.negative? ? "DECLINING #{trend}" : "FLAT")
|
|
318
331
|
puts " trend vs #{prior_comparable['repo_sha']} (same corpus+dims): #{arrow}"
|
|
@@ -321,7 +334,7 @@ unless quiet
|
|
|
321
334
|
else
|
|
322
335
|
puts " trend: first run — no history yet"
|
|
323
336
|
end
|
|
324
|
-
puts " (
|
|
337
|
+
puts " (inspect dimensions and task evidence; gates block independently; history=#{history}, git-ignored)"
|
|
325
338
|
end
|
|
326
339
|
|
|
327
340
|
exit 0
|
|
@@ -299,7 +299,11 @@ end
|
|
|
299
299
|
# changed owner at all, so the entire row evaluation was skipped and the
|
|
300
300
|
# surviving row was never even looked at. With no ledger change there are no
|
|
301
301
|
# added rows, so the skip stays exactly as cheap as before for unrelated diffs.
|
|
302
|
-
|
|
302
|
+
# The routing task bank joins the entry condition (045): a commit that moves only
|
|
303
|
+
# `eval/routing-tasks.jsonl` changes no owner package and need not touch the
|
|
304
|
+
# ledger, so without this the whole block — including the bank-evidence
|
|
305
|
+
# obligation written for exactly that change — was never entered.
|
|
306
|
+
if upstream.any? || changed_paths.include?(LEDGER_PATH) || changed_paths.include?("eval/routing-tasks.jsonl")
|
|
303
307
|
# Rows are collected PER ROUND and carry the scope they were authored against,
|
|
304
308
|
# so the classifiers below judge a row against its own round's diff. Reading the
|
|
305
309
|
# cumulative register diff instead would re-judge every earlier round's rows
|
|
@@ -1338,4 +1342,302 @@ if upstream.any? || changed_paths.include?(LEDGER_PATH)
|
|
|
1338
1342
|
behavior_failures.each { |path| warn " incomplete: #{path}" }
|
|
1339
1343
|
exit 1
|
|
1340
1344
|
end
|
|
1345
|
+
|
|
1346
|
+
# ---- 045: an obligation whose skip leaves no artifact is not enforced ------
|
|
1347
|
+
# Round 044 withdrew several overclaims and, in the same move, demoted the
|
|
1348
|
+
# obligations riding on them into advisories. Five consecutive challenge rounds
|
|
1349
|
+
# returned one shape: the entry condition is stated by the party the obligation
|
|
1350
|
+
# constrains, and SKIPPING IT PRODUCES NOTHING for a reviewer to notice missing.
|
|
1351
|
+
# A reviewer cannot refuse an absent claim.
|
|
1352
|
+
#
|
|
1353
|
+
# Both checks below key on facts of the DIFF — which file changed, which key a
|
|
1354
|
+
# row carries — never on prose semantics. That is deliberate and is the whole
|
|
1355
|
+
# difference from the proxy predicates this repository has repeatedly watched
|
|
1356
|
+
# get walked through: a proxy asks "does this text look like it did the thing",
|
|
1357
|
+
# an invariant asks "is the artifact there". Skipping either obligation now
|
|
1358
|
+
# leaves a detectable absence, which is the half the demotion removed.
|
|
1359
|
+
#
|
|
1360
|
+
# Scope limit, stated here so a later round does not read more into it: these
|
|
1361
|
+
# close OMISSION, not MISCLASSIFICATION. `result-class` is still chosen by the
|
|
1362
|
+
# author, and a bank-evidence locator is not proof the bank actually ran. What
|
|
1363
|
+
# changes is that a silent skip becomes a visible claim the review can accept
|
|
1364
|
+
# or refuse — which is exactly what round 044's reviewer said was missing.
|
|
1365
|
+
fragment_value = lambda do |text, key|
|
|
1366
|
+
text.to_s.split(";").each do |fragment|
|
|
1367
|
+
m = fragment.match(/\A\s*#{Regexp.escape(key)}:\s*(.+?)\s*\z/i)
|
|
1368
|
+
return m[1] if m
|
|
1369
|
+
end
|
|
1370
|
+
nil
|
|
1371
|
+
end
|
|
1372
|
+
declaring_row = lambda do |row|
|
|
1373
|
+
row[:behavior].to_s.split(";").any? { |f| f.match?(/\A\s*behavioral-evidence:/i) }
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
result_classes = %w[failure stable-success insufficient-evidence]
|
|
1377
|
+
# A round is held only to the grammar ITS OWN HEAD declares. Adding a required
|
|
1378
|
+
# field would otherwise refuse every historical round the moment anyone replays
|
|
1379
|
+
# it — which the verdict-differential suite correctly reports as a regression,
|
|
1380
|
+
# and which would make the row grammar permanently unextendable. This is not a
|
|
1381
|
+
# date or commit-id grandfather clause (those are proxies that drift): the
|
|
1382
|
+
# predicate is whether the ledger at that round's head defines the field, so a
|
|
1383
|
+
# round that was never told cannot be refused for not knowing.
|
|
1384
|
+
grammar_marker = "result-class: failure|stable-success|insufficient-evidence"
|
|
1385
|
+
grammar_declared_cache = {}
|
|
1386
|
+
grammar_declared_at = lambda do |ref|
|
|
1387
|
+
grammar_declared_cache[ref] ||= begin
|
|
1388
|
+
bytes = raw_blob_at.call(ref, LEDGER_PATH)
|
|
1389
|
+
next false unless bytes
|
|
1390
|
+
# The DECLARATION is prose, and only prose declares. Scanning the whole blob
|
|
1391
|
+
# let a round delete the declaring paragraph while dropping the same literal
|
|
1392
|
+
# into an ordinary ledger ROW: both sides then read as declared, the
|
|
1393
|
+
# withdrawal check stays empty, and every later round keeps being judged by
|
|
1394
|
+
# a grammar the authority no longer states. Rows quote and describe; they do
|
|
1395
|
+
# not legislate, so a marker sitting inside one is not a declaration.
|
|
1396
|
+
bytes.dup.force_encoding(Encoding::UTF_8).each_line.any? do |line|
|
|
1397
|
+
!line.lstrip.start_with?("|") && line.include?(grammar_marker)
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1400
|
+
end
|
|
1401
|
+
# Scoped to rows that resolve to an owner THIS round changed — the same subject
|
|
1402
|
+
# set every other per-row check uses. Walking `rows` instead swept in every
|
|
1403
|
+
# historical ledger line whenever the round's base did not already contain it
|
|
1404
|
+
# (a shallow fixture base, a branch cut far back), turning an append-only ledger
|
|
1405
|
+
# into something that fails on its own past. The obligation is on the round
|
|
1406
|
+
# being judged, not on the file it appends to.
|
|
1407
|
+
# Declared -> undeclared is not a legitimate history, it is the off switch.
|
|
1408
|
+
# Without this, deleting or rewording the paragraph makes both checks skip and
|
|
1409
|
+
# the round is indistinguishable from one that predates them — a silent
|
|
1410
|
+
# disarm wearing the shape of the very non-retroactivity rule that permits it.
|
|
1411
|
+
# Withdrawing the grammar deliberately is still possible; it just cannot be
|
|
1412
|
+
# done quietly, which is the whole subject of this round.
|
|
1413
|
+
grammar_withdrawn = round_bounds.select do |span_base, span_head|
|
|
1414
|
+
grammar_declared_at.call(span_base) && !grammar_declared_at.call(span_head)
|
|
1415
|
+
end
|
|
1416
|
+
unless grammar_withdrawn.empty?
|
|
1417
|
+
warn "impact_chain_grammar_withdrawn: a round removed the ledger's declaration-fragment paragraph, which would disable `result-class` and `bank-evidence` for every later round"
|
|
1418
|
+
warn " note: the checks are gated on the head-declared grammar so history is not judged by rules it was never told. That gate reads an ABSENT declaration as `before the rule`; a declaration that was present and is now gone is a withdrawal, and it has to be argued rather than committed"
|
|
1419
|
+
# Name only a remediation this gate actually implements. An earlier version of
|
|
1420
|
+
# this line offered "land the withdrawal with a named risk owner", and no code
|
|
1421
|
+
# here parses any such artifact — the refusal was unconditional, so the
|
|
1422
|
+
# message promised a way through that did not exist and the round had none.
|
|
1423
|
+
# The escape is deliberately NOT built: a data-side waiver token for a
|
|
1424
|
+
# mechanical obligation is the self-adjudication surface this round removes.
|
|
1425
|
+
# Withdrawing the obligation means removing the triggers, which is a reviewable
|
|
1426
|
+
# code diff rather than a line someone writes about themselves.
|
|
1427
|
+
warn " fix: restore the `result-class: failure|stable-success|insufficient-evidence` definition in #{LEDGER_PATH}. There is no data-side waiver: to retire the obligation, remove the checks in this file in their own reviewed change"
|
|
1428
|
+
grammar_withdrawn.each { |base, head| warn " round: #{base}..#{head}" }
|
|
1429
|
+
exit 1
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
# EVERY declaring row of a round whose head declares the grammar, not only rows
|
|
1433
|
+
# that resolve to a changed owner. Scoping by owner was the first attempt at
|
|
1434
|
+
# non-retroactivity, and it left its own hole: a round that changes only the
|
|
1435
|
+
# routing bank has no changed owner, so a declaring row with no `result-class`
|
|
1436
|
+
# was never examined at all. Non-retroactivity is the grammar gate's job and it
|
|
1437
|
+
# does that job alone; making the owner set do it too bought nothing and cost a
|
|
1438
|
+
# false green. A row that declares an impact chain owes its class, whatever the
|
|
1439
|
+
# round happened to touch.
|
|
1440
|
+
result_class_failures = rows.select do |row|
|
|
1441
|
+
next false unless declaring_row.call(row)
|
|
1442
|
+
next false unless grammar_declared_at.call(row[:scope].head)
|
|
1443
|
+
value = fragment_value.call(row[:behavior], "result-class")
|
|
1444
|
+
!(value && result_classes.include?(value.downcase))
|
|
1445
|
+
end.uniq { |row| row[:line] }
|
|
1446
|
+
unless result_class_failures.empty?
|
|
1447
|
+
warn "impact_chain_result_class_missing: an added row declaring behavioral-evidence carries no usable `result-class`"
|
|
1448
|
+
warn " note: omitting the cell was the cheapest of the three bypasses review found — static checks pass, nothing emits `interim`, and the reviewer has no field to accept or refuse. The value stays the author's call; its ABSENCE no longer does"
|
|
1449
|
+
warn " fix: add `result-class: #{result_classes.join('|')}` to the row's declaration fragments"
|
|
1450
|
+
result_class_failures.each { |row| warn " row: #{row[:line].to_s.sub(/^\+/, '').strip}" }
|
|
1451
|
+
exit 1
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
# Trigger A: a change to the routing surface owes bank evidence.
|
|
1455
|
+
bank_relative = "eval/routing-tasks.jsonl"
|
|
1456
|
+
description_entry_at = lambda do |ref, relative|
|
|
1457
|
+
bytes = raw_blob_at.call(ref, relative)
|
|
1458
|
+
next nil if bytes.nil?
|
|
1459
|
+
parts = split_frontmatter.call(bytes.dup.force_encoding(Encoding::UTF_8))
|
|
1460
|
+
next nil unless parts
|
|
1461
|
+
entry = split_description_entry.call(parts[0])
|
|
1462
|
+
entry && entry[1]
|
|
1463
|
+
end
|
|
1464
|
+
# `upstream` keys are owner-relative (`<owner>/SKILL.md`) while changed_paths and
|
|
1465
|
+
# blob lookups are repo-relative; the same `skills/` prefix the evidence-file
|
|
1466
|
+
# check applies. Without it the description branch silently never fired and the
|
|
1467
|
+
# two cases that DO carry evidence passed for the wrong reason — a false green
|
|
1468
|
+
# that only the paired no-evidence cases could expose.
|
|
1469
|
+
# Owner-scoped ONLY. An earlier version also returned true here whenever the bank
|
|
1470
|
+
# had changed, on the reasoning that the bank is everyone's routing surface. In a
|
|
1471
|
+
# multi-round range that conflated two different subjects: the round that moved
|
|
1472
|
+
# the bank then demanded an owner-specific row from every owner the RANGE had
|
|
1473
|
+
# touched, including owners changed in some other round for unrelated reasons —
|
|
1474
|
+
# so adding a later, unrelated commit changed the verdict on an earlier one. The
|
|
1475
|
+
# bank is its own subject and is checked as one; this predicate answers only
|
|
1476
|
+
# "did THIS owner move ITS OWN description in THIS round".
|
|
1477
|
+
description_touched = lambda do |scope, path|
|
|
1478
|
+
relative = "skills/#{path}"
|
|
1479
|
+
next false unless scope.changed_paths.include?(relative)
|
|
1480
|
+
base_entry = description_entry_at.call(scope.base, relative)
|
|
1481
|
+
head_entry = description_entry_at.call(scope.head, relative)
|
|
1482
|
+
# "No head entry" is two different things and only one of them is exempt.
|
|
1483
|
+
# The FILE being gone (deleted, renamed away) is exempt: its description did
|
|
1484
|
+
# not move, the file did, and the rename machinery owns that case. The file
|
|
1485
|
+
# still being there with its description entry REMOVED is the opposite — the
|
|
1486
|
+
# skill stops advertising what reaches it, which is a routing-surface change
|
|
1487
|
+
# as much as rewording one. Collapsing both into `head_entry.nil?` let a
|
|
1488
|
+
# description be deleted in place with no evidence and no refusal.
|
|
1489
|
+
if head_entry.nil?
|
|
1490
|
+
next false if raw_blob_at.call(scope.head, relative).nil?
|
|
1491
|
+
next !base_entry.nil?
|
|
1492
|
+
end
|
|
1493
|
+
# A path with a head entry and NO base entry is a routing surface being
|
|
1494
|
+
# CREATED — a first description decides which requests reach a new skill, so
|
|
1495
|
+
# it is the largest routing-surface change there is, not an exemption. An
|
|
1496
|
+
# earlier version skipped it as "conservative"; review named that as the hole
|
|
1497
|
+
# it is. The one creation that is not a new routing surface is a rename's
|
|
1498
|
+
# target, where the same description arrives under a new path — git proves
|
|
1499
|
+
# that pairing, so it is excluded on evidence rather than on shape.
|
|
1500
|
+
if base_entry.nil?
|
|
1501
|
+
pair = scope.rename_pairs.find { |_from, to| "#{to}/SKILL.md" == path }
|
|
1502
|
+
if pair
|
|
1503
|
+
# A rename is excluded because the SAME description arrived under a new
|
|
1504
|
+
# path. Excluding the target without checking that made rename+reword in
|
|
1505
|
+
# one commit a free routing change: the target is exempt as "just a
|
|
1506
|
+
# rename" and the source is exempt for having no head entry, so the edit
|
|
1507
|
+
# rides in between them. Compare across the pair — the exemption is for
|
|
1508
|
+
# a description that did not change, not for a path that moved.
|
|
1509
|
+
from_entry = description_entry_at.call(scope.base, "skills/#{pair[0]}/SKILL.md")
|
|
1510
|
+
next true if from_entry.nil?
|
|
1511
|
+
next from_entry != head_entry
|
|
1512
|
+
end
|
|
1513
|
+
next true
|
|
1514
|
+
end
|
|
1515
|
+
base_entry != head_entry
|
|
1516
|
+
end
|
|
1517
|
+
# A downscope is allowed — skipping the measurement is sometimes right — but it
|
|
1518
|
+
# must land somewhere a reader reaches. Requiring the token in a changed spec
|
|
1519
|
+
# keeps `downscoped:` from becoming the next self-adjudicated exit: the author
|
|
1520
|
+
# still decides, and still has to write the decision down where review sees it.
|
|
1521
|
+
downscope_recorded = lambda do |scope, token|
|
|
1522
|
+
# The token has to be a TOKEN. `\S+` accepted `-`, and a substring search then
|
|
1523
|
+
# found it in the bullet punctuation of any changed spec line — so
|
|
1524
|
+
# `downscoped:-` discharged the obligation against a record that says nothing.
|
|
1525
|
+
# A downscope is a decision someone has to be able to look up; a character
|
|
1526
|
+
# that occurs incidentally in prose cannot be that. Eight or more, starting
|
|
1527
|
+
# alphanumeric, and no whitespace: long enough not to collide by accident,
|
|
1528
|
+
# loose enough not to dictate a naming scheme.
|
|
1529
|
+
next false unless token.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]{7,}\z/)
|
|
1530
|
+
# The spec must record the DECLARATION, verbatim — `downscoped:<token>` — not
|
|
1531
|
+
# merely contain the token somewhere. Searching for the bare token let a
|
|
1532
|
+
# common word discharge the obligation against a line that happens to use it
|
|
1533
|
+
# ("Bank evidence anchor …" satisfied `downscoped:evidence`), which is the
|
|
1534
|
+
# silent self-adjudication this gate exists to close, rebuilt out of substring
|
|
1535
|
+
# matching. Requiring the full spelling makes the recorded line say what it is.
|
|
1536
|
+
# Bounded, not substring. `include?("downscoped:TOK")` also matched a spec that
|
|
1537
|
+
# records `downscoped:TOKLONGER` — a strict prefix of somebody else's token
|
|
1538
|
+
# discharging this row — and matched `not-downscoped:TOK` too, which records
|
|
1539
|
+
# the opposite. The declaration has to end where the row's token ends and to
|
|
1540
|
+
# start at the keyword.
|
|
1541
|
+
needle = /(?<![A-Za-z0-9._-])downscoped:#{Regexp.escape(token)}(?![A-Za-z0-9._-])/
|
|
1542
|
+
scope.changed_paths.any? do |relative|
|
|
1543
|
+
next false unless relative.start_with?("specs/") && relative.end_with?(".md")
|
|
1544
|
+
added_lines_for.call(scope, relative).any? { |line| line.match?(needle) }
|
|
1545
|
+
end
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
# Evaluated PER ROUND, not per owner. Two defects independent review found in
|
|
1549
|
+
# the per-owner shape, both of which let the obligation evaporate:
|
|
1550
|
+
# - a diff that changes ONLY `eval/routing-tasks.jsonl` has no changed owner,
|
|
1551
|
+
# so the per-owner loop was never entered and the routing bank could move
|
|
1552
|
+
# with no evidence at all. The first fixture missed it by also touching an
|
|
1553
|
+
# owner, which is how a hole hides behind a green case.
|
|
1554
|
+
# - `triggered` and `satisfied` each scanned every row of the owner across ALL
|
|
1555
|
+
# rounds, so one round's valid locator satisfied a different round's naked
|
|
1556
|
+
# routing-surface change. The obligation belongs to the round that incurred
|
|
1557
|
+
# it; the row that discharges it has to sit in the same round.
|
|
1558
|
+
bank_evidence_failures = []
|
|
1559
|
+
# Iterating the ROUNDS, not the rows: grouping by row scope meant a round that
|
|
1560
|
+
# moved the bank and appended no ledger row had no scope to inspect, so the one
|
|
1561
|
+
# change that needs no owner and no row escaped entirely. The first fixture for
|
|
1562
|
+
# this hid it by also appending a row — twice now a green case has stood in
|
|
1563
|
+
# front of the hole it was written to cover.
|
|
1564
|
+
round_bounds.each do |span_base, span_head|
|
|
1565
|
+
scope = scope_at.call(span_base, span_head)
|
|
1566
|
+
scope_rows = rows.select { |row| row[:scope].head == span_head }
|
|
1567
|
+
# Same head-declared-grammar rule as `result-class` above, and for the same
|
|
1568
|
+
# reason: the paragraph that defines `bank-evidence` also dates it.
|
|
1569
|
+
next unless grammar_declared_at.call(scope.head)
|
|
1570
|
+
bank_changed = scope.changed_paths.include?(bank_relative)
|
|
1571
|
+
triggered_owners = (upstream + lineage_extra.keys).uniq.select do |path|
|
|
1572
|
+
description_touched.call(scope, path)
|
|
1573
|
+
end
|
|
1574
|
+
# A package that did not exist at the round's base is not in the owner set —
|
|
1575
|
+
# the set is built from CHANGED owners, and a brand-new skill has nothing to
|
|
1576
|
+
# have changed. Its first description is still a routing surface being
|
|
1577
|
+
# created, so it is picked up from the changed paths directly rather than
|
|
1578
|
+
# being silently outside the trigger because of how the owner set is built.
|
|
1579
|
+
created_surfaces = scope.changed_paths.filter_map do |relative|
|
|
1580
|
+
next unless relative.start_with?("skills/") && relative.end_with?("/SKILL.md")
|
|
1581
|
+
path = relative.sub(%r{\Askills/}, "")
|
|
1582
|
+
next if triggered_owners.include?(path)
|
|
1583
|
+
next unless description_touched.call(scope, path)
|
|
1584
|
+
path
|
|
1585
|
+
end
|
|
1586
|
+
triggered_owners |= created_surfaces
|
|
1587
|
+
next if !bank_changed && triggered_owners.empty?
|
|
1588
|
+
# Discharged PER SUBJECT, not per round. Scanning the whole round meant that
|
|
1589
|
+
# when two owners changed their descriptions, one owner's valid locator
|
|
1590
|
+
# marked the round satisfied and the other could omit its evidence entirely —
|
|
1591
|
+
# the obligation is incurred by each owner that moved its routing surface, so
|
|
1592
|
+
# each has to carry its own.
|
|
1593
|
+
row_satisfies = lambda do |row, self_owner|
|
|
1594
|
+
value = fragment_value.call(row[:behavior], "bank-evidence")
|
|
1595
|
+
next false unless value
|
|
1596
|
+
if (m = value.match(/\Adownscoped:(\S+)\z/i))
|
|
1597
|
+
downscope_recorded.call(scope, m[1])
|
|
1598
|
+
else
|
|
1599
|
+
# A locator pointing back into the owner's own package is the change, not
|
|
1600
|
+
# evidence about it. Without this a description edit satisfies its own
|
|
1601
|
+
# bank obligation by citing the very line it just wrote — an artifact that
|
|
1602
|
+
# exists and proves nothing, which is the shape this whole round removes.
|
|
1603
|
+
# Checked on the PATH after stripping the kind prefix, not on the `file:`
|
|
1604
|
+
# spelling: the first version matched `file:skills/<owner>/` only, so an
|
|
1605
|
+
# owner that also touched a script in its own package could cite it as
|
|
1606
|
+
# `command:skills/<owner>/scripts/…` and validate the change as evidence
|
|
1607
|
+
# about itself. Both locator forms name a path; the ban is about the path.
|
|
1608
|
+
if self_owner
|
|
1609
|
+
located = value.sub(/\A(?:command|file):/i, "").sub(/#.*\z/m, "")
|
|
1610
|
+
# Normalize before the prefix test. A raw `start_with?` compares one
|
|
1611
|
+
# spelling of a path, and `./skills/<owner>/…` or a `a/../` detour names
|
|
1612
|
+
# the same file while missing the check — the ban is on the LOCATION, so
|
|
1613
|
+
# it has to be decided on a canonical form of it.
|
|
1614
|
+
segments = []
|
|
1615
|
+
located.split("/").each do |seg|
|
|
1616
|
+
next if seg.empty? || seg == "."
|
|
1617
|
+
seg == ".." ? segments.pop : segments << seg
|
|
1618
|
+
end
|
|
1619
|
+
next false if segments.take(2) == ["skills", self_owner]
|
|
1620
|
+
end
|
|
1621
|
+
locator_valid.call(scope, value)
|
|
1622
|
+
end
|
|
1623
|
+
end
|
|
1624
|
+
triggered_owners.each do |path|
|
|
1625
|
+
owner = path.sub(%r{/SKILL\.md\z}, "")
|
|
1626
|
+
owner_scope_rows = rows_by_upstream_path[path].select { |row| row[:scope].head == scope.head }
|
|
1627
|
+
bank_evidence_failures << path unless owner_scope_rows.any? { |row| row_satisfies.call(row, owner) }
|
|
1628
|
+
end
|
|
1629
|
+
if bank_changed
|
|
1630
|
+
# The bank is nobody's package, so any row of this round may discharge it;
|
|
1631
|
+
# what it may not be is absent.
|
|
1632
|
+
bank_evidence_failures << bank_relative unless scope_rows.any? { |row| row_satisfies.call(row, nil) }
|
|
1633
|
+
end
|
|
1634
|
+
end
|
|
1635
|
+
bank_evidence_failures.uniq!
|
|
1636
|
+
unless bank_evidence_failures.empty?
|
|
1637
|
+
warn "impact_chain_bank_evidence_missing: a round that changed the routing surface carries no bank evidence for that owner"
|
|
1638
|
+
warn " note: the routing surface is the SKILL.md frontmatter `description` entry and `#{bank_relative}`. The measurement protocol says it is mandatory, but nothing produced or consumed it, so not running it left no absence to detect — the reviewer saw a candidate, not a gap"
|
|
1639
|
+
warn " fix: add `bank-evidence: command:<changed executable>` or `bank-evidence: file:<changed markdown>#<unique anchor>` to that owner's row; to skip the run deliberately use `bank-evidence: downscoped:<token>` and record the same token in this round's spec, so the downscope is itself an artifact"
|
|
1640
|
+
bank_evidence_failures.each { |path| warn " owes bank evidence: #{path}" }
|
|
1641
|
+
exit 1
|
|
1642
|
+
end
|
|
1341
1643
|
end
|