@ccoalm/ccl-skills 0.3.0 → 0.4.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/skill-extraction-workflow/SKILL.md +8 -8
- 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/extraction-quickstart.md +3 -2
- 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 +29 -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 +153 -0
- 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 +6 -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/release.json +39 -24
- package/package.json +1 -1
|
@@ -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
|