@ccoalm/ccl-skills 0.9.0 → 0.11.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/SKILL.md +4 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +23 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +178 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +127 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +11 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +9 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +17 -20
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +26 -44
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +24 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +5 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +59 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +35 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-contract-anchors.sh +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +197 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +210 -36
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/gate_receipt.py +576 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +35 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_antipattern_grep_panel.sh +80 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +99 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +81 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +28 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +251 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_contract_anchors.sh +196 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +222 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +16 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity.sh +178 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity_selfproof.sh +108 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_gate_receipt.sh +431 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_pinned_phrase_mutation_walk.sh +151 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +336 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +86 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +41 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +141 -28
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +139 -38
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
- package/dist/assets/release.json +110 -45
- package/package.json +1 -1
|
@@ -140,11 +140,12 @@ def make_fixture(
|
|
|
140
140
|
receipt_mutators=None,
|
|
141
141
|
completion=True,
|
|
142
142
|
completion_mutator=None,
|
|
143
|
-
budget=
|
|
143
|
+
budget=1,
|
|
144
144
|
base_shas=None,
|
|
145
145
|
closeout=None,
|
|
146
146
|
unmatched=0,
|
|
147
147
|
open_last=False,
|
|
148
|
+
succession=False,
|
|
148
149
|
):
|
|
149
150
|
if receipt_findings is None:
|
|
150
151
|
receipt_findings = [[finding(1)], []]
|
|
@@ -166,11 +167,15 @@ def make_fixture(
|
|
|
166
167
|
receipt_hashes = []
|
|
167
168
|
receipt_refs = []
|
|
168
169
|
for index, findings in enumerate(receipt_findings, start=1):
|
|
169
|
-
|
|
170
|
+
# With a succession the LAST round opens a second chain: its own chain
|
|
171
|
+
# arithmetic restarts at one while its lane position keeps counting.
|
|
172
|
+
is_succession = succession and index == rounds
|
|
173
|
+
chain_position = 1 if is_succession else index
|
|
174
|
+
mode = "challenge" if is_succession else "review" if index == 1 else "challenge"
|
|
170
175
|
status = "findings" if findings else "passed"
|
|
171
|
-
remaining =
|
|
176
|
+
remaining = 2 - chain_position
|
|
172
177
|
if status == "findings":
|
|
173
|
-
state = "post_review_budget" if
|
|
178
|
+
state = "post_review_budget" if chain_position == 2 else "findings_pending"
|
|
174
179
|
else:
|
|
175
180
|
state = "reviewed"
|
|
176
181
|
receipt = {
|
|
@@ -179,16 +184,27 @@ def make_fixture(
|
|
|
179
184
|
"status": status,
|
|
180
185
|
"findings": copy.deepcopy(findings),
|
|
181
186
|
"review_chain_tracked": True,
|
|
182
|
-
"review_chain_id":
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
"review_chain_id": (
|
|
188
|
+
"extraction-succession" if is_succession else "extraction-chain"
|
|
189
|
+
),
|
|
190
|
+
"autonomous_review_index": chain_position,
|
|
191
|
+
"autonomous_review_budget": 2,
|
|
185
192
|
"autonomous_reviews_remaining": remaining,
|
|
186
193
|
"autonomous_review_allowed": remaining > 0,
|
|
187
|
-
"challenge_index": 0 if index == 1 else index - 1,
|
|
194
|
+
"challenge_index": 1 if is_succession else 0 if index == 1 else index - 1,
|
|
188
195
|
"challenge_budget": budget,
|
|
189
|
-
"prior_review_result_sha256": list(receipt_hashes),
|
|
190
|
-
"
|
|
191
|
-
"
|
|
196
|
+
"prior_review_result_sha256": [] if is_succession else list(receipt_hashes),
|
|
197
|
+
"predecessor_chain_id": "extraction-chain" if is_succession else None,
|
|
198
|
+
"predecessor_result_sha256": (
|
|
199
|
+
receipt_hashes[-1] if is_succession and receipt_hashes else None
|
|
200
|
+
),
|
|
201
|
+
"predecessor_candidate_sha256": OTHER_CANDIDATE if is_succession else None,
|
|
202
|
+
"candidate_sha256": (
|
|
203
|
+
OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
|
|
204
|
+
),
|
|
205
|
+
"packet_sha256": (
|
|
206
|
+
OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
|
|
207
|
+
),
|
|
192
208
|
"review_scope": copy.deepcopy(scope),
|
|
193
209
|
"review_scope_sha256": scope_sha,
|
|
194
210
|
"wording_only_proof_sha256": None,
|
|
@@ -221,7 +237,7 @@ def make_fixture(
|
|
|
221
237
|
"review_chain_tracked": True,
|
|
222
238
|
"review_chain_id": final["review_chain_id"],
|
|
223
239
|
"autonomous_review_index": final["autonomous_review_index"],
|
|
224
|
-
"autonomous_review_budget":
|
|
240
|
+
"autonomous_review_budget": 2,
|
|
225
241
|
"autonomous_reviews_remaining": final["autonomous_reviews_remaining"],
|
|
226
242
|
"autonomous_review_allowed": False,
|
|
227
243
|
"challenge_budget": budget,
|
|
@@ -411,9 +427,9 @@ run("float-schema", float_schema["ledger"], 1, "schema_version must be 3")
|
|
|
411
427
|
|
|
412
428
|
float_budget = make_fixture(
|
|
413
429
|
"float-budget",
|
|
414
|
-
receipt_mutators={2: lambda row: row.update(challenge_budget=
|
|
430
|
+
receipt_mutators={2: lambda row: row.update(challenge_budget=1.0)},
|
|
415
431
|
)
|
|
416
|
-
run("float-budget", float_budget["ledger"], 1, "challenge_budget must be
|
|
432
|
+
run("float-budget", float_budget["ledger"], 1, "challenge_budget must be 1")
|
|
417
433
|
|
|
418
434
|
nan_schema = make_fixture("nan-schema")
|
|
419
435
|
nan_schema["ledger"]["schema_version"] = float("nan")
|
|
@@ -493,11 +509,108 @@ scope_mismatch = make_fixture("scope-mismatch", receipt_mutators={2: drift_scope
|
|
|
493
509
|
run("scope-mismatch", scope_mismatch["ledger"], 1, "review scope changed")
|
|
494
510
|
|
|
495
511
|
budget_four = make_fixture("budget-four", budget=4)
|
|
496
|
-
run("budget-four", budget_four["ledger"], 1, "challenge_budget must be
|
|
512
|
+
run("budget-four", budget_four["ledger"], 1, "challenge_budget must be 1")
|
|
497
513
|
|
|
498
514
|
review_only = make_fixture("review-only", receipt_findings=[[]])
|
|
499
515
|
run("review-only", review_only["ledger"], 1, "ready requires at least one tracked challenge")
|
|
500
516
|
|
|
517
|
+
# A caller must not out-run the wrapper budget by supplying extra receipts: a
|
|
518
|
+
# third receipt under budget 1 claims a negative remaining count and would
|
|
519
|
+
# otherwise reach completion validation as a ready-state budget bypass.
|
|
520
|
+
over_budget = make_fixture("over-budget", receipt_findings=[[], [], []])
|
|
521
|
+
run("over-budget", over_budget["ledger"], 1, "exceeds the wrapper budget")
|
|
522
|
+
|
|
523
|
+
# Chain succession. A fix that edits the reviewed owner package ends its chain, so
|
|
524
|
+
# the landing candidate can only be challenged in a succeeding chain. The lane then
|
|
525
|
+
# spans two chains and three rounds, and the succession round is the only one that
|
|
526
|
+
# binds what actually lands.
|
|
527
|
+
succession_ready = make_fixture(
|
|
528
|
+
"succession-ready", receipt_findings=[[finding(1)], [], []], succession=True
|
|
529
|
+
)
|
|
530
|
+
run("succession-ready", succession_ready["ledger"], 0, "closeout_state=ready_for_human_decision")
|
|
531
|
+
|
|
532
|
+
# The failure this whole mechanism exists to make visible: the fix batch moved the
|
|
533
|
+
# candidate and no round ever inspected what lands.
|
|
534
|
+
succession_absent = make_fixture(
|
|
535
|
+
"succession-absent",
|
|
536
|
+
receipt_findings=[[finding(1)], []],
|
|
537
|
+
receipt_mutators={
|
|
538
|
+
1: lambda row: row.update(
|
|
539
|
+
candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
|
|
540
|
+
),
|
|
541
|
+
2: lambda row: row.update(
|
|
542
|
+
candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
|
|
543
|
+
),
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
run("succession-absent", succession_absent["ledger"], 1, "does not bind the ledger candidate")
|
|
547
|
+
|
|
548
|
+
# A succession whose predecessor saw the same candidate reviewed nothing new.
|
|
549
|
+
succession_unmoved = make_fixture(
|
|
550
|
+
"succession-unmoved",
|
|
551
|
+
receipt_findings=[[finding(1)], [], []],
|
|
552
|
+
succession=True,
|
|
553
|
+
receipt_mutators={
|
|
554
|
+
1: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
|
|
555
|
+
2: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
|
|
556
|
+
3: lambda row: row.update(predecessor_candidate_sha256=CANDIDATE),
|
|
557
|
+
},
|
|
558
|
+
)
|
|
559
|
+
run(
|
|
560
|
+
"succession-unmoved",
|
|
561
|
+
succession_unmoved["ledger"],
|
|
562
|
+
1,
|
|
563
|
+
"must bind a candidate the wrapper chain never saw",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
succession_foreign_terminal = make_fixture(
|
|
567
|
+
"succession-foreign-terminal",
|
|
568
|
+
receipt_findings=[[finding(1)], [], []],
|
|
569
|
+
succession=True,
|
|
570
|
+
receipt_mutators={3: lambda row: row.update(predecessor_result_sha256="f" * 64)},
|
|
571
|
+
)
|
|
572
|
+
run(
|
|
573
|
+
"succession-foreign-terminal",
|
|
574
|
+
succession_foreign_terminal["ledger"],
|
|
575
|
+
1,
|
|
576
|
+
"does not bind the succeeded chain's terminal receipt",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
succession_self = make_fixture(
|
|
580
|
+
"succession-self",
|
|
581
|
+
receipt_findings=[[finding(1)], [], []],
|
|
582
|
+
succession=True,
|
|
583
|
+
receipt_mutators={3: lambda row: row.update(review_chain_id="extraction-chain")},
|
|
584
|
+
)
|
|
585
|
+
run("succession-self", succession_self["ledger"], 1, "succeeds its own chain")
|
|
586
|
+
|
|
587
|
+
succession_misplaced = make_fixture(
|
|
588
|
+
"succession-misplaced",
|
|
589
|
+
receipt_findings=[[finding(1)], [], []],
|
|
590
|
+
succession=True,
|
|
591
|
+
receipt_mutators={
|
|
592
|
+
2: lambda row: row.update(predecessor_chain_id="extraction-chain"),
|
|
593
|
+
},
|
|
594
|
+
)
|
|
595
|
+
run(
|
|
596
|
+
"succession-misplaced",
|
|
597
|
+
succession_misplaced["ledger"],
|
|
598
|
+
1,
|
|
599
|
+
"must be the lane's final round",
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
succession_over_lane = make_fixture(
|
|
603
|
+
"succession-over-lane",
|
|
604
|
+
receipt_findings=[[finding(1)], [], [], []],
|
|
605
|
+
succession=True,
|
|
606
|
+
)
|
|
607
|
+
run(
|
|
608
|
+
"succession-over-lane",
|
|
609
|
+
succession_over_lane["ledger"],
|
|
610
|
+
1,
|
|
611
|
+
"controller_receipts must contain one to 3 ordered Agent rounds",
|
|
612
|
+
)
|
|
613
|
+
|
|
501
614
|
missing_complete = make_fixture("missing-complete")
|
|
502
615
|
missing_complete["ledger"]["completion_receipt"] = None
|
|
503
616
|
run("missing-complete", missing_complete["ledger"], 1, "requires a completion receipt")
|
|
@@ -537,7 +650,7 @@ run(
|
|
|
537
650
|
|
|
538
651
|
continuation = make_fixture(
|
|
539
652
|
"continuation",
|
|
540
|
-
receipt_findings=[[finding(1)], [
|
|
653
|
+
receipt_findings=[[finding(1)], [finding(2)]],
|
|
541
654
|
completion=False,
|
|
542
655
|
open_last=True,
|
|
543
656
|
)
|
|
@@ -550,10 +663,10 @@ run(
|
|
|
550
663
|
|
|
551
664
|
continuation_unconsumed_first_drift = make_fixture(
|
|
552
665
|
"continuation-unconsumed-first-drift",
|
|
553
|
-
receipt_findings=[[finding(1)], [
|
|
666
|
+
receipt_findings=[[finding(1)], [finding(2)]],
|
|
554
667
|
completion=False,
|
|
555
668
|
open_last=True,
|
|
556
|
-
base_shas=[A, A,
|
|
669
|
+
base_shas=[A, A, B],
|
|
557
670
|
)
|
|
558
671
|
run(
|
|
559
672
|
"continuation-unconsumed-first-drift",
|
|
@@ -564,7 +677,7 @@ run(
|
|
|
564
677
|
|
|
565
678
|
continuation_candidate = make_fixture(
|
|
566
679
|
"continuation-candidate",
|
|
567
|
-
receipt_findings=[[finding(1)], [
|
|
680
|
+
receipt_findings=[[finding(1)], [finding(2)]],
|
|
568
681
|
completion=False,
|
|
569
682
|
open_last=True,
|
|
570
683
|
)
|
|
@@ -595,8 +708,8 @@ run(
|
|
|
595
708
|
|
|
596
709
|
unknown_state = make_fixture(
|
|
597
710
|
"unknown-state",
|
|
598
|
-
receipt_findings=[[finding(1)], [
|
|
599
|
-
receipt_mutators={
|
|
711
|
+
receipt_findings=[[finding(1)], [finding(2)]],
|
|
712
|
+
receipt_mutators={2: lambda row: row.update(review_state="potato")},
|
|
600
713
|
completion=False,
|
|
601
714
|
open_last=True,
|
|
602
715
|
)
|
|
@@ -605,10 +718,10 @@ run("unknown-state", unknown_state["ledger"], 1, "unknown controller review_stat
|
|
|
605
718
|
|
|
606
719
|
passed_continuation = make_fixture(
|
|
607
720
|
"passed-continuation",
|
|
608
|
-
receipt_findings=[[finding(1)], []
|
|
721
|
+
receipt_findings=[[finding(1)], []],
|
|
609
722
|
completion=False,
|
|
610
723
|
)
|
|
611
|
-
run("passed-continuation", passed_continuation["ledger"], 1, "
|
|
724
|
+
run("passed-continuation", passed_continuation["ledger"], 1, "findings in post_review_budget")
|
|
612
725
|
|
|
613
726
|
bad_finding_hash = make_fixture("bad-finding-hash")
|
|
614
727
|
bad_finding_hash["ledger"]["finding_classes"][0]["occurrences"][0][
|
|
@@ -744,7 +857,7 @@ run(
|
|
|
744
857
|
|
|
745
858
|
historical_open = make_fixture(
|
|
746
859
|
"historical-open",
|
|
747
|
-
receipt_findings=[[finding(1)
|
|
860
|
+
receipt_findings=[[finding(1), finding(2)], []],
|
|
748
861
|
)
|
|
749
862
|
historical_open["ledger"]["finding_classes"][0]["occurrences"][0][
|
|
750
863
|
"disposition"
|
|
@@ -756,7 +869,7 @@ run("historical-open", historical_open["ledger"], 1, "unresolved finding occurre
|
|
|
756
869
|
|
|
757
870
|
historical_open_linked = make_fixture(
|
|
758
871
|
"historical-open-linked",
|
|
759
|
-
receipt_findings=[[finding(1)
|
|
872
|
+
receipt_findings=[[finding(1), finding(2)], []],
|
|
760
873
|
)
|
|
761
874
|
historical_open_linked_occurrences = historical_open_linked["ledger"][
|
|
762
875
|
"finding_classes"
|
|
@@ -825,7 +938,7 @@ run("needs-human", needs_human["ledger"], 1, "unresolved finding occurrence")
|
|
|
825
938
|
|
|
826
939
|
third_occurrence = make_fixture(
|
|
827
940
|
"third-occurrence",
|
|
828
|
-
receipt_findings=[[finding(1)
|
|
941
|
+
receipt_findings=[[finding(1), finding(2)], [finding(3)]],
|
|
829
942
|
completion=False,
|
|
830
943
|
unmatched=1,
|
|
831
944
|
open_last=True,
|
|
@@ -1045,7 +1158,7 @@ for closing_disposition in sorted(CLOSED_DISPOSITIONS):
|
|
|
1045
1158
|
case_name = f"historical-needs-human-{closing_disposition.replace('_', '-')}"
|
|
1046
1159
|
historical_needs_human_linked = make_fixture(
|
|
1047
1160
|
case_name,
|
|
1048
|
-
receipt_findings=[[finding(1)
|
|
1161
|
+
receipt_findings=[[finding(1), finding(2)], []],
|
|
1049
1162
|
)
|
|
1050
1163
|
historical_needs_human_occurrences = historical_needs_human_linked["ledger"][
|
|
1051
1164
|
"finding_classes"
|
|
@@ -1128,7 +1241,7 @@ duplicate_completion_result = invoke(
|
|
|
1128
1241
|
|
|
1129
1242
|
duplicate_sweep = make_fixture(
|
|
1130
1243
|
"duplicate-sweep",
|
|
1131
|
-
receipt_findings=[[finding(1)
|
|
1244
|
+
receipt_findings=[[finding(1), finding(2)], [finding(3)]],
|
|
1132
1245
|
completion=False,
|
|
1133
1246
|
open_last=True,
|
|
1134
1247
|
)
|
|
@@ -30,6 +30,17 @@ TERMINAL_STATES = {
|
|
|
30
30
|
}
|
|
31
31
|
EXTERNAL_REVIEW_STATES = {"reviewed", "findings_pending", "post_review_budget"}
|
|
32
32
|
KNOWN_REVIEW_STATES = EXTERNAL_REVIEW_STATES | {"self_reviewed"}
|
|
33
|
+
# The extraction wrapper fixes the autonomous lane at one review plus one
|
|
34
|
+
# challenge; every numeric bound below derives from these two constants so a
|
|
35
|
+
# future budget change lands in exactly one place.
|
|
36
|
+
WRAPPER_CHALLENGE_BUDGET = 1
|
|
37
|
+
WRAPPER_AUTONOMOUS_ROUNDS = WRAPPER_CHALLENGE_BUDGET + 1
|
|
38
|
+
# A fix that edits the reviewed owner package ends its chain by construction, so
|
|
39
|
+
# the post-fix candidate is challenged in ONE succeeding chain rather than inside
|
|
40
|
+
# the ended one. The lane therefore spans at most two chains: the wrapper-budget
|
|
41
|
+
# chain plus a single succession challenge bound to the landing candidate.
|
|
42
|
+
LANE_MAX_CHAINS = 2
|
|
43
|
+
LANE_MAX_ROUNDS = WRAPPER_AUTONOMOUS_ROUNDS + 1
|
|
33
44
|
DISPOSITIONS = {
|
|
34
45
|
"fixed",
|
|
35
46
|
"source_refuted",
|
|
@@ -272,8 +283,8 @@ def validate_scope(receipt: dict[str, Any], label: str) -> str:
|
|
|
272
283
|
]
|
|
273
284
|
if len(normalized_risks) != len(set(normalized_risks)):
|
|
274
285
|
fail(f"{label}.review_scope.risk_tags contains duplicates")
|
|
275
|
-
if scope["challenge_budget"] !=
|
|
276
|
-
fail(f"{label}.review_scope.challenge_budget must be
|
|
286
|
+
if scope["challenge_budget"] != WRAPPER_CHALLENGE_BUDGET or type(scope["challenge_budget"]) is not int:
|
|
287
|
+
fail(f"{label}.review_scope.challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
|
|
277
288
|
if (
|
|
278
289
|
scope["wording_only_proof_sha256"] is not None
|
|
279
290
|
or scope["wording_only_scope_sha256"] is not None
|
|
@@ -299,8 +310,10 @@ def validate_controller_receipts(
|
|
|
299
310
|
payload: dict[str, Any], ledger_dir: Path
|
|
300
311
|
) -> tuple[list[dict[str, Any]], list[str], dict[str, list[str]], str, str]:
|
|
301
312
|
refs = payload["controller_receipts"]
|
|
302
|
-
if not isinstance(refs, list) or not 1 <= len(refs) <=
|
|
303
|
-
fail(
|
|
313
|
+
if not isinstance(refs, list) or not 1 <= len(refs) <= LANE_MAX_ROUNDS:
|
|
314
|
+
fail(
|
|
315
|
+
f"controller_receipts must contain one to {LANE_MAX_ROUNDS} ordered Agent rounds"
|
|
316
|
+
)
|
|
304
317
|
if payload["autonomous_round"] != len(refs) or type(payload["autonomous_round"]) is not int:
|
|
305
318
|
fail("autonomous_round must equal the ordered controller receipt count")
|
|
306
319
|
|
|
@@ -308,11 +321,20 @@ def validate_controller_receipts(
|
|
|
308
321
|
receipt_hashes: list[str] = []
|
|
309
322
|
finding_hashes: dict[str, list[str]] = {}
|
|
310
323
|
chain_id: str | None = None
|
|
324
|
+
final_chain_id: str | None = None
|
|
311
325
|
scope_hash: str | None = None
|
|
326
|
+
candidates: list[str] = []
|
|
327
|
+
succession_index: int | None = None
|
|
312
328
|
ledger_candidate = sha256(
|
|
313
329
|
payload["candidate_sha256"], "ledger.candidate_sha256"
|
|
314
330
|
)
|
|
315
331
|
for expected_index, value in enumerate(refs, start=1):
|
|
332
|
+
# A caller supplying more receipts than the lane can mint would
|
|
333
|
+
# otherwise claim a negative remaining count and reach completion
|
|
334
|
+
# validation as a ready-state budget bypass. The wrapper bound still
|
|
335
|
+
# governs each chain; the lane bound governs their sum.
|
|
336
|
+
if expected_index > LANE_MAX_ROUNDS:
|
|
337
|
+
fail("controller chain exceeds the lane budget")
|
|
316
338
|
ref = exact_object(
|
|
317
339
|
value, {"sequence", "file", "sha256"}, f"controller_receipts[{expected_index - 1}]"
|
|
318
340
|
)
|
|
@@ -329,7 +351,28 @@ def validate_controller_receipts(
|
|
|
329
351
|
receipt = decode_json(raw, label=f"controller receipt {expected_index}")
|
|
330
352
|
if receipt.get("schema_version") != 3 or type(receipt.get("schema_version")) is not int:
|
|
331
353
|
fail(f"controller receipt {expected_index} schema_version must be 3")
|
|
332
|
-
|
|
354
|
+
# A succession round opens a second chain, so its own chain arithmetic
|
|
355
|
+
# restarts at one while its lane position keeps counting. Classify first;
|
|
356
|
+
# every expectation below reads the chain position, not the lane index.
|
|
357
|
+
predecessor_chain = receipt.get("predecessor_chain_id")
|
|
358
|
+
is_succession = predecessor_chain is not None
|
|
359
|
+
if is_succession:
|
|
360
|
+
if expected_index != len(refs):
|
|
361
|
+
fail("a succession round must be the lane's final round")
|
|
362
|
+
if expected_index != WRAPPER_AUTONOMOUS_ROUNDS + 1:
|
|
363
|
+
fail("a succession round may only follow a spent wrapper chain")
|
|
364
|
+
succession_index = expected_index
|
|
365
|
+
chain_position = 1
|
|
366
|
+
else:
|
|
367
|
+
# Only a succession round may sit past the wrapper chain. Without this
|
|
368
|
+
# the caller claims a negative remaining count and reaches completion
|
|
369
|
+
# validation as a ready-state budget bypass.
|
|
370
|
+
if expected_index > WRAPPER_AUTONOMOUS_ROUNDS:
|
|
371
|
+
fail("controller chain exceeds the wrapper budget")
|
|
372
|
+
chain_position = expected_index
|
|
373
|
+
expected_mode = (
|
|
374
|
+
"challenge" if is_succession else "review" if expected_index == 1 else "challenge"
|
|
375
|
+
)
|
|
333
376
|
if receipt.get("mode") != expected_mode:
|
|
334
377
|
fail(f"controller receipt {expected_index} must have mode {expected_mode}")
|
|
335
378
|
if receipt.get("status") not in {"passed", "findings"}:
|
|
@@ -339,26 +382,35 @@ def validate_controller_receipts(
|
|
|
339
382
|
current_chain = bounded_text(
|
|
340
383
|
receipt.get("review_chain_id"), f"controller receipt {expected_index}.review_chain_id", 120
|
|
341
384
|
)
|
|
342
|
-
if
|
|
385
|
+
if is_succession:
|
|
386
|
+
if current_chain == chain_id:
|
|
387
|
+
fail(
|
|
388
|
+
f"controller receipt {expected_index} succeeds its own chain"
|
|
389
|
+
)
|
|
390
|
+
final_chain_id = current_chain
|
|
391
|
+
elif chain_id is None:
|
|
343
392
|
chain_id = current_chain
|
|
393
|
+
final_chain_id = current_chain
|
|
344
394
|
elif current_chain != chain_id:
|
|
345
395
|
fail(f"controller receipt {expected_index} review_chain_id changed")
|
|
346
|
-
if receipt.get("autonomous_review_index") !=
|
|
396
|
+
if receipt.get("autonomous_review_index") != chain_position or type(
|
|
347
397
|
receipt.get("autonomous_review_index")
|
|
348
398
|
) is not int:
|
|
349
399
|
fail(f"controller receipt {expected_index} autonomous_review_index is not contiguous")
|
|
350
|
-
expected_challenge_index =
|
|
400
|
+
expected_challenge_index = (
|
|
401
|
+
1 if is_succession else 0 if expected_index == 1 else expected_index - 1
|
|
402
|
+
)
|
|
351
403
|
if receipt.get("challenge_index") != expected_challenge_index or type(
|
|
352
404
|
receipt.get("challenge_index")
|
|
353
405
|
) is not int:
|
|
354
406
|
fail(f"controller receipt {expected_index} challenge_index is invalid")
|
|
355
|
-
if receipt.get("challenge_budget") !=
|
|
356
|
-
fail(f"controller receipt {expected_index} challenge_budget must be
|
|
357
|
-
if receipt.get("autonomous_review_budget") !=
|
|
407
|
+
if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
|
|
408
|
+
fail(f"controller receipt {expected_index} challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
|
|
409
|
+
if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
|
|
358
410
|
receipt.get("autonomous_review_budget")
|
|
359
411
|
) is not int:
|
|
360
|
-
fail(f"controller receipt {expected_index} autonomous_review_budget must be
|
|
361
|
-
expected_remaining =
|
|
412
|
+
fail(f"controller receipt {expected_index} autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
|
|
413
|
+
expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - chain_position
|
|
362
414
|
if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
|
|
363
415
|
receipt.get("autonomous_reviews_remaining")
|
|
364
416
|
) is not int:
|
|
@@ -366,7 +418,8 @@ def validate_controller_receipts(
|
|
|
366
418
|
if receipt.get("autonomous_review_allowed") is not (expected_remaining > 0):
|
|
367
419
|
fail(f"controller receipt {expected_index} autonomous_review_allowed is invalid")
|
|
368
420
|
prior = receipt.get("prior_review_result_sha256")
|
|
369
|
-
if
|
|
421
|
+
expected_prior = [] if is_succession else receipt_hashes
|
|
422
|
+
if prior != expected_prior:
|
|
370
423
|
fail(f"controller receipt {expected_index} prior_review_result_sha256 is not the complete ordered prefix")
|
|
371
424
|
candidate = sha256(
|
|
372
425
|
receipt.get("candidate_sha256"), f"controller receipt {expected_index}.candidate_sha256"
|
|
@@ -376,13 +429,10 @@ def validate_controller_receipts(
|
|
|
376
429
|
)
|
|
377
430
|
if packet != candidate:
|
|
378
431
|
fail(f"controller receipt {expected_index} packet_sha256 must equal candidate_sha256")
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
|
|
383
|
-
fail(
|
|
384
|
-
f"controller receipt {expected_index} does not bind the ledger candidate"
|
|
385
|
-
)
|
|
432
|
+
# Which candidate a round must bind depends on its phase, and the phase
|
|
433
|
+
# split is only known once every round is loaded, so this is settled
|
|
434
|
+
# after the loop rather than here.
|
|
435
|
+
candidates.append(candidate)
|
|
386
436
|
current_scope_hash = validate_scope(receipt, f"controller receipt {expected_index}")
|
|
387
437
|
if scope_hash is None:
|
|
388
438
|
scope_hash = current_scope_hash
|
|
@@ -415,7 +465,7 @@ def validate_controller_receipts(
|
|
|
415
465
|
fail(f"controller receipt {expected_index} has unknown controller review_state {state}")
|
|
416
466
|
expected_state = (
|
|
417
467
|
"post_review_budget"
|
|
418
|
-
if receipt["status"] == "findings" and
|
|
468
|
+
if receipt["status"] == "findings" and chain_position == WRAPPER_AUTONOMOUS_ROUNDS
|
|
419
469
|
else "findings_pending"
|
|
420
470
|
if receipt["status"] == "findings"
|
|
421
471
|
else "reviewed"
|
|
@@ -429,9 +479,38 @@ def validate_controller_receipts(
|
|
|
429
479
|
receipt_hashes.append(receipt_hash)
|
|
430
480
|
finding_hashes[receipt_hash] = current_findings
|
|
431
481
|
|
|
432
|
-
if chain_id is None or scope_hash is None:
|
|
482
|
+
if chain_id is None or scope_hash is None or final_chain_id is None:
|
|
433
483
|
fail("controller receipt chain is empty")
|
|
434
|
-
|
|
484
|
+
|
|
485
|
+
# Phase binding. Without a succession every counted round must have inspected
|
|
486
|
+
# the exact final candidate. With one, the wrapper chain reviewed the candidate
|
|
487
|
+
# the fix batch then moved, and the succession round is the only round that can
|
|
488
|
+
# bind what actually lands — which is the whole point of running it.
|
|
489
|
+
if succession_index is None:
|
|
490
|
+
for position, value in enumerate(candidates, start=1):
|
|
491
|
+
if value != ledger_candidate:
|
|
492
|
+
fail(f"controller receipt {position} does not bind the ledger candidate")
|
|
493
|
+
else:
|
|
494
|
+
succession = receipts[-1]
|
|
495
|
+
succeeded_candidate = sha256(
|
|
496
|
+
succession.get("predecessor_candidate_sha256"),
|
|
497
|
+
"succession receipt.predecessor_candidate_sha256",
|
|
498
|
+
)
|
|
499
|
+
if candidates[-1] != ledger_candidate:
|
|
500
|
+
fail("the succession round does not bind the ledger candidate")
|
|
501
|
+
if succeeded_candidate == ledger_candidate:
|
|
502
|
+
fail("a succession round must bind a candidate the wrapper chain never saw")
|
|
503
|
+
for position, value in enumerate(candidates[:-1], start=1):
|
|
504
|
+
if value != succeeded_candidate:
|
|
505
|
+
fail(f"controller receipt {position} does not bind the succeeded candidate")
|
|
506
|
+
if succession.get("predecessor_chain_id") != chain_id:
|
|
507
|
+
fail("the succession round does not name the chain it succeeds")
|
|
508
|
+
if sha256(
|
|
509
|
+
succession.get("predecessor_result_sha256"),
|
|
510
|
+
"succession receipt.predecessor_result_sha256",
|
|
511
|
+
) != receipt_hashes[-2]:
|
|
512
|
+
fail("the succession round does not bind the succeeded chain's terminal receipt")
|
|
513
|
+
return receipts, receipt_hashes, finding_hashes, final_chain_id, scope_hash
|
|
435
514
|
|
|
436
515
|
|
|
437
516
|
def validate_completion_receipt(
|
|
@@ -469,24 +548,30 @@ def validate_completion_receipt(
|
|
|
469
548
|
fail("completion receipt cannot close a final external receipt with findings")
|
|
470
549
|
if receipt.get("review_chain_tracked") is not True or receipt.get("review_chain_id") != chain_id:
|
|
471
550
|
fail("completion receipt review_chain_id does not match the controller chain")
|
|
472
|
-
if receipt.get("challenge_budget") !=
|
|
473
|
-
fail("completion receipt challenge_budget must be
|
|
474
|
-
if receipt.get("autonomous_review_budget") !=
|
|
551
|
+
if receipt.get("challenge_budget") != WRAPPER_CHALLENGE_BUDGET or type(receipt.get("challenge_budget")) is not int:
|
|
552
|
+
fail(f"completion receipt challenge_budget must be {WRAPPER_CHALLENGE_BUDGET}")
|
|
553
|
+
if receipt.get("autonomous_review_budget") != WRAPPER_AUTONOMOUS_ROUNDS or type(
|
|
475
554
|
receipt.get("autonomous_review_budget")
|
|
476
555
|
) is not int:
|
|
477
|
-
fail("completion receipt autonomous_review_budget must be
|
|
478
|
-
|
|
556
|
+
fail(f"completion receipt autonomous_review_budget must be {WRAPPER_AUTONOMOUS_ROUNDS}")
|
|
557
|
+
# The completion checkpoint binds the final external round, so its chain
|
|
558
|
+
# arithmetic is that round's — which is the succeeding chain's when a fix
|
|
559
|
+
# batch ended the wrapper chain, not the lane's round count.
|
|
560
|
+
final_index = final.get("autonomous_review_index")
|
|
561
|
+
if receipt.get("autonomous_review_index") != final_index or type(
|
|
479
562
|
receipt.get("autonomous_review_index")
|
|
480
563
|
) is not int:
|
|
481
564
|
fail("completion receipt autonomous_review_index does not match the final round")
|
|
482
|
-
expected_remaining =
|
|
565
|
+
expected_remaining = WRAPPER_AUTONOMOUS_ROUNDS - final_index
|
|
483
566
|
if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
|
|
484
567
|
receipt.get("autonomous_reviews_remaining")
|
|
485
568
|
) is not int:
|
|
486
569
|
fail("completion receipt autonomous_reviews_remaining does not match the final round")
|
|
487
570
|
if receipt.get("autonomous_review_allowed") is not False:
|
|
488
571
|
fail("completion receipt must disable further autonomous review")
|
|
489
|
-
if receipt.get("prior_review_result_sha256") !=
|
|
572
|
+
if receipt.get("prior_review_result_sha256") != final.get(
|
|
573
|
+
"prior_review_result_sha256"
|
|
574
|
+
):
|
|
490
575
|
fail("completion receipt prior_review_result_sha256 does not match the final external receipt")
|
|
491
576
|
if receipt.get("completion_review_result_sha256") != receipt_hashes[-1]:
|
|
492
577
|
fail("completion receipt completion_review_result_sha256 does not identify the final external receipt")
|
|
@@ -940,13 +1025,29 @@ def validate(payload: dict[str, Any], ledger_dir: Path) -> tuple[str, int, int]:
|
|
|
940
1025
|
fail("ready_for_human_decision requires no unresolved finding occurrence and no unreviewed delta")
|
|
941
1026
|
elif closeout == "continuation_authorization_required":
|
|
942
1027
|
final = receipts[-1]
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
)
|
|
949
|
-
|
|
1028
|
+
# The lane is spent either at the wrapper chain's last round, or — when a
|
|
1029
|
+
# fix batch moved the candidate — at the succession round that reviewed it.
|
|
1030
|
+
# Only the closeout knows the lane is spent: the controller sizes
|
|
1031
|
+
# human_decision_required from its own chain, where the succession round
|
|
1032
|
+
# is round one of two.
|
|
1033
|
+
succeeded = final.get("predecessor_chain_id") is not None
|
|
1034
|
+
if succeeded:
|
|
1035
|
+
spent = (
|
|
1036
|
+
len(receipts) == LANE_MAX_ROUNDS and final.get("status") == "findings"
|
|
1037
|
+
)
|
|
1038
|
+
else:
|
|
1039
|
+
spent = (
|
|
1040
|
+
len(receipts) == WRAPPER_AUTONOMOUS_ROUNDS
|
|
1041
|
+
and final.get("status") == "findings"
|
|
1042
|
+
and final.get("review_state") == "post_review_budget"
|
|
1043
|
+
and final.get("human_decision_required") is True
|
|
1044
|
+
)
|
|
1045
|
+
if not spent:
|
|
1046
|
+
fail(
|
|
1047
|
+
"continuation_authorization_required requires final-round "
|
|
1048
|
+
f"(round {WRAPPER_AUTONOMOUS_ROUNDS}) findings in post_review_budget, "
|
|
1049
|
+
f"or a succession round (round {LANE_MAX_ROUNDS}) carrying findings"
|
|
1050
|
+
)
|
|
950
1051
|
elif closeout == "baseline_race" and not delta:
|
|
951
1052
|
fail("baseline_race requires a non-empty unreviewed_delta")
|
|
952
1053
|
|
|
@@ -15,7 +15,7 @@ Bitable ownership:
|
|
|
15
15
|
|
|
16
16
|
- Ambiguous "写测试用例" / "write test cases" requests mean `testing-strategy` first when the user wants test code, E2E/browser/device tests, CI coverage, mocks, or verification evidence. Use this skill only when the deliverable is a structured TC artifact, Feishu/Bitable sync, or source-to-TC generation workflow.
|
|
17
17
|
- Use `testing-strategy` to decide which test layers (unit / integration / E2E / scenario) are needed *after* the use cases are written.
|
|
18
|
-
-
|
|
18
|
+
- Test-code implementation, once test cases are approved, must go to the owning stack skill: `go-microservice-dev` / `python-service-dev` / `nodejs-service-dev` / `web-react-dev` / `miniapp-product-dev` / `app-cross-platform-dev`.
|
|
19
19
|
- Use `product-rd-workflow` when the test case work is part of a broader feature delivery plan.
|
|
20
20
|
- Use `llm-inference-integration` when test cases cover LLM / agent / RAG / inference behavior.
|
|
21
21
|
- Use `skill-extraction-workflow` to feed lessons from test case generation or Feishu Bitable setup back into this skill after each delivery cycle.
|
|
@@ -9,14 +9,15 @@ Use this skill when the task is about test scope, test design, verification evid
|
|
|
9
9
|
|
|
10
10
|
## Skill Routing
|
|
11
11
|
|
|
12
|
-
- Use this skill for test strategy, test pyramid decisions, unit/integration/e2e/scenario boundaries, fixtures, mocks/fakes, regression coverage, and verification evidence.
|
|
13
|
-
- Use `go-microservice-dev` for Go-specific implementation details
|
|
14
|
-
- Use `python-service-dev` for Python-specific pytest, async test, fixture, fake client, ruff, mypy/pyright, packaging, or migration-test implementation details
|
|
15
|
-
-
|
|
16
|
-
- Use `
|
|
17
|
-
- Use `
|
|
18
|
-
- Use `
|
|
19
|
-
- Use `
|
|
12
|
+
- Use this skill for test strategy, test pyramid decisions, unit/integration/e2e/scenario boundaries, fixtures, mocks/fakes, regression coverage, and verification evidence. Stack pointers below apply after that choice.
|
|
13
|
+
- Use `go-microservice-dev` for Go-specific implementation details.
|
|
14
|
+
- Use `python-service-dev` for Python-specific pytest, async test, fixture, fake client, ruff, mypy/pyright, packaging, or migration-test implementation details.
|
|
15
|
+
- Node runner, mock/fixture, fake timers, stream and cancellation assertions, child-process lifecycle, and packaged-artifact verification must use `nodejs-service-dev`.
|
|
16
|
+
- Use `app-cross-platform-dev` for Flutter unit/widget/integration tests, Android unit/Compose/instrumented tests, iOS unit/UI tests, mobile device smoke, and app release verification.
|
|
17
|
+
- Use `miniapp-product-dev` for WeChat/Alipay/Douyin/Baidu mini-program build, developer-tool preview, real-device smoke, host capability verification, review submission, and release checks.
|
|
18
|
+
- Use `web-react-dev` for React unit/component/API-client/browser/E2E implementation, browser automation, accessibility checks, and build/deploy smoke.
|
|
19
|
+
- Use `terminal-cli-dev` for the terminal interface contract, PTY/ANSI/keyboard rendering, and full-screen text UI testing.
|
|
20
|
+
- Use `llm-inference-integration` for LLM/agent/RAG evals, replay sets, prompt/model regression checks, shadow runs, and inference-specific observability.
|
|
20
21
|
- Use `defect-diagnosis` first when a test is failing, flaky, or used to reproduce a bug.
|
|
21
22
|
- Use `product-rd-workflow` first when testing is part of an end-to-end product delivery plan.
|
|
22
23
|
- Use `product-ui-ux-design` for visual hierarchy, interaction, state, accessibility, and design acceptance criteria; use this skill for automated proof and test coverage.
|