@ccoalm/ccl-skills 0.11.0 → 0.13.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/defect-diagnosis/SKILL.md +21 -12
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/references/diagnosis-playbook.md +42 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +7 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-context-freshness.md +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-tool-dispatch.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/inference-capacity-operations.md +20 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/llm-client-gateway.md +11 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/model-prompt-evaluation.md +3 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/retrieval-agent-safety.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/SKILL.md +13 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/references/multi-agent-delegation-playbook.md +12 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +14 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +38 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +364 -23
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +185 -0
- package/dist/assets/release.json +20 -20
- package/package.json +1 -1
|
@@ -44,6 +44,32 @@ commit; the alternative costs a review round nobody can reproduce. The workflow
|
|
|
44
44
|
directory stays bound, as it was before: with only `skills/` bound, deleting the CI
|
|
45
45
|
step that runs this gate would not move the candidate the evidence has to match.
|
|
46
46
|
|
|
47
|
+
The candidate's Git identity and the reviewer's input are two different sizes.
|
|
48
|
+
A landing candidate is base..HEAD and has no natural byte limit; a review packet
|
|
49
|
+
is what one reviewer can read whole, and the controller caps it at
|
|
50
|
+
`MAX_PACKET_BYTES`. Binding the landing candidate to ONE packet hash therefore
|
|
51
|
+
made the reviewer's ceiling the pull request's ceiling: a candidate larger than
|
|
52
|
+
one packet could not be frozen, no ledger could ever bind it, and authors split
|
|
53
|
+
the pull request instead of the review -- eight merges for one release. The
|
|
54
|
+
review side already allowed splitting a large candidate by path into partitions
|
|
55
|
+
(the `code-review` skill's packet rule); what was missing was the merge side
|
|
56
|
+
consuming them. A committed landing partition manifest closes that: it names
|
|
57
|
+
path partitions whose changed files together equal the candidate's changed files
|
|
58
|
+
exactly once, and each partition's `candidate_sha256`, which is what
|
|
59
|
+
`--print-candidate --paths <partition>` already answers. This gate recomputes
|
|
60
|
+
every partition with the same `freeze_packet`, requires a validator-accepted
|
|
61
|
+
ledger per partition, and refuses any manifest whose parts do not add up to the
|
|
62
|
+
whole: an uncovered file, an overlapping file, a partition that no longer
|
|
63
|
+
reproduces, a base other than the fork point, or an aggregate hash that does not
|
|
64
|
+
reproduce its own partitions. The manifest carries a top-level 64-hex
|
|
65
|
+
`candidate_sha256` -- the aggregate identity -- so it satisfies the existing
|
|
66
|
+
receipt predicate and committing it moves no partition; that is the load-bearing
|
|
67
|
+
reason for the field, and the exclusion predicate itself is unchanged. What the
|
|
68
|
+
manifest proves is the same narrow thing the single ledger proves, taken per
|
|
69
|
+
part: every byte that lands is a byte some external round froze and inspected.
|
|
70
|
+
Merge-queue aggregation of several pull requests into one HEAD is a different
|
|
71
|
+
aggregate and remains unsolved here.
|
|
72
|
+
|
|
47
73
|
Boundaries this gate does NOT close, stated because a gate that lives inside the
|
|
48
74
|
candidate cannot authenticate itself: it cannot prove the caller retained every
|
|
49
75
|
earlier chain; it runs the candidate's own validator, so a candidate that also
|
|
@@ -92,6 +118,21 @@ EVIDENCE_MEMBER = re.compile(r"^specs/[^/]+/evidence/")
|
|
|
92
118
|
# A receipt is small; anything larger is not one, and reading it is not free.
|
|
93
119
|
MAX_RECEIPT_BYTES = 4_000_000
|
|
94
120
|
DEFAULT_PATHS = (".",)
|
|
121
|
+
# A landing partition manifest splits one candidate by path into packets a
|
|
122
|
+
# reviewer can read whole. Its shape is closed: exactly these keys, this kind,
|
|
123
|
+
# and a bounded partition count, so a manifest cannot double as a ledger or
|
|
124
|
+
# smuggle fields the gate does not read.
|
|
125
|
+
MANIFEST_KIND = "landing_partition_manifest"
|
|
126
|
+
MANIFEST_SCHEMA_VERSION = 1
|
|
127
|
+
MANIFEST_KEYS = {"schema_version", "kind", "base", "partitions", "candidate_sha256"}
|
|
128
|
+
PARTITION_KEYS = {"paths", "candidate_sha256"}
|
|
129
|
+
MAX_PARTITIONS = 64
|
|
130
|
+
HEX40 = re.compile(r"^[0-9a-f]{40}$")
|
|
131
|
+
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ManifestError(Exception):
|
|
135
|
+
"""A manifest that does not describe this candidate; the message is the reason."""
|
|
95
136
|
|
|
96
137
|
|
|
97
138
|
def emit(message: str) -> None:
|
|
@@ -312,6 +353,233 @@ def candidate_hash(module: types.ModuleType, repo_root: Path, base: str, paths:
|
|
|
312
353
|
return packet_sha256
|
|
313
354
|
|
|
314
355
|
|
|
356
|
+
def canonical_digest(value: object) -> str:
|
|
357
|
+
canonical = json.dumps(
|
|
358
|
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
359
|
+
).encode("utf-8")
|
|
360
|
+
return hashlib.sha256(canonical).hexdigest()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def manifest_aggregate(base: str, partitions: list[dict]) -> str:
|
|
364
|
+
"""The aggregate identity a manifest carries as its own `candidate_sha256`.
|
|
365
|
+
|
|
366
|
+
Computed here and nowhere else: the renderer writes it and the gate rechecks
|
|
367
|
+
it, so two implementations of one hash cannot drift into a forged manifest.
|
|
368
|
+
"""
|
|
369
|
+
return canonical_digest(
|
|
370
|
+
{
|
|
371
|
+
"schema_version": MANIFEST_SCHEMA_VERSION,
|
|
372
|
+
"kind": MANIFEST_KIND,
|
|
373
|
+
"base": base,
|
|
374
|
+
"partitions": partitions,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def validate_partition_path(value: object) -> str:
|
|
380
|
+
"""A partition path is a plain relative path, never a pathspec.
|
|
381
|
+
|
|
382
|
+
The gate appends its own exclusions; a manifest that could name `:(exclude)`
|
|
383
|
+
or a glob would choose what its partition does not cover, and a leading `-`
|
|
384
|
+
would reach git as an option. Reject the shape here rather than trust git to
|
|
385
|
+
interpret it the way the manifest author hoped.
|
|
386
|
+
"""
|
|
387
|
+
if not isinstance(value, str) or not value:
|
|
388
|
+
raise ManifestError("partition path must be a non-empty string")
|
|
389
|
+
if value.startswith((":", "-", "/")) or any(ord(ch) < 32 for ch in value):
|
|
390
|
+
raise ManifestError(f"partition path is not a plain relative path: {value!r}")
|
|
391
|
+
# Git reads `*`, `?`, `[` and `\` inside a pathspec as glob/escape syntax even
|
|
392
|
+
# without magic; a wildcard partition would let the manifest choose its own
|
|
393
|
+
# coverage, so it is refused as a path rather than handed to git.
|
|
394
|
+
if any(ch in value for ch in "*?[]\\"):
|
|
395
|
+
raise ManifestError(f"partition path contains a pathspec wildcard: {value!r}")
|
|
396
|
+
if ".." in value.split("/"):
|
|
397
|
+
raise ManifestError(f"partition path escapes the repository: {value!r}")
|
|
398
|
+
return value
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def parse_manifest(payload: dict, fork: str) -> tuple[list[list[str]], list[str]]:
|
|
402
|
+
"""Return (partition path lists, partition hashes) or raise ManifestError.
|
|
403
|
+
|
|
404
|
+
Order of checks is cheapest first and each failure names one reason, so an
|
|
405
|
+
author reads which part failed to add up rather than a generic refusal.
|
|
406
|
+
"""
|
|
407
|
+
if set(payload) != MANIFEST_KEYS:
|
|
408
|
+
raise ManifestError("manifest does not carry exactly the manifest keys")
|
|
409
|
+
if payload["schema_version"] != MANIFEST_SCHEMA_VERSION or type(payload["schema_version"]) is not int:
|
|
410
|
+
raise ManifestError(f"manifest schema_version must be {MANIFEST_SCHEMA_VERSION}")
|
|
411
|
+
base = payload["base"]
|
|
412
|
+
if not isinstance(base, str) or not HEX40.match(base):
|
|
413
|
+
raise ManifestError("manifest base must be a 40-hex commit id")
|
|
414
|
+
if base != fork:
|
|
415
|
+
raise ManifestError(
|
|
416
|
+
f"manifest base {base[:12]} is not this candidate's fork point {fork[:12]}"
|
|
417
|
+
)
|
|
418
|
+
partitions = payload["partitions"]
|
|
419
|
+
if not isinstance(partitions, list) or not 1 <= len(partitions) <= MAX_PARTITIONS:
|
|
420
|
+
raise ManifestError(f"manifest must list between 1 and {MAX_PARTITIONS} partitions")
|
|
421
|
+
path_lists: list[list[str]] = []
|
|
422
|
+
digests: list[str] = []
|
|
423
|
+
seen: set[str] = set()
|
|
424
|
+
for index, partition in enumerate(partitions, start=1):
|
|
425
|
+
if not isinstance(partition, dict) or set(partition) != PARTITION_KEYS:
|
|
426
|
+
raise ManifestError(f"partition {index} does not carry exactly paths and candidate_sha256")
|
|
427
|
+
paths = partition["paths"]
|
|
428
|
+
if not isinstance(paths, list) or not paths:
|
|
429
|
+
raise ManifestError(f"partition {index} names no paths")
|
|
430
|
+
validated = [validate_partition_path(value) for value in paths]
|
|
431
|
+
for value in validated:
|
|
432
|
+
if value in seen:
|
|
433
|
+
raise ManifestError(f"partition path is listed twice: {value}")
|
|
434
|
+
seen.add(value)
|
|
435
|
+
digest = partition["candidate_sha256"]
|
|
436
|
+
if not isinstance(digest, str) or not HEX64.match(digest):
|
|
437
|
+
raise ManifestError(f"partition {index} candidate_sha256 must be 64-hex")
|
|
438
|
+
path_lists.append(validated)
|
|
439
|
+
digests.append(digest)
|
|
440
|
+
aggregate = payload["candidate_sha256"]
|
|
441
|
+
if aggregate != manifest_aggregate(base, partitions):
|
|
442
|
+
raise ManifestError("manifest aggregate candidate_sha256 does not reproduce its partitions")
|
|
443
|
+
return path_lists, digests
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def partition_coverage(
|
|
447
|
+
repo_root: Path,
|
|
448
|
+
base: str,
|
|
449
|
+
path_lists: list[list[str]],
|
|
450
|
+
excludes: tuple[str, ...],
|
|
451
|
+
changed_all: list[str],
|
|
452
|
+
) -> list[list[str]]:
|
|
453
|
+
"""Each partition's changed files; refuse unless they tile the candidate.
|
|
454
|
+
|
|
455
|
+
Name-only diffs are cheap, so every way the parts can fail to add up is found
|
|
456
|
+
before any packet is frozen: an empty partition, a changed file in no
|
|
457
|
+
partition, or a changed file in two. Overlap is refused rather than tolerated
|
|
458
|
+
because two verdicts over one file leave undefined which one covers it.
|
|
459
|
+
"""
|
|
460
|
+
per_partition: list[list[str]] = []
|
|
461
|
+
owner: dict[str, int] = {}
|
|
462
|
+
overlaps: list[str] = []
|
|
463
|
+
for index, paths in enumerate(path_lists, start=1):
|
|
464
|
+
changed = changed_skill_paths(repo_root, base, tuple(paths) + excludes)
|
|
465
|
+
if not changed:
|
|
466
|
+
raise ManifestError(f"partition {index} ({' '.join(paths)}) covers no changed path")
|
|
467
|
+
for value in changed:
|
|
468
|
+
if value in owner:
|
|
469
|
+
overlaps.append(value)
|
|
470
|
+
owner[value] = index
|
|
471
|
+
per_partition.append(changed)
|
|
472
|
+
if overlaps:
|
|
473
|
+
raise ManifestError(
|
|
474
|
+
"partitions overlap on changed paths: " + ", ".join(sorted(set(overlaps))[:5])
|
|
475
|
+
)
|
|
476
|
+
uncovered = sorted(set(changed_all) - set(owner))
|
|
477
|
+
if uncovered:
|
|
478
|
+
raise ManifestError(
|
|
479
|
+
"changed paths uncovered by every partition: " + ", ".join(uncovered[:5])
|
|
480
|
+
)
|
|
481
|
+
# Equality, not containment: under a narrowed --paths scope a partition can
|
|
482
|
+
# reach changed files outside the reviewed set, and a union larger than the
|
|
483
|
+
# candidate is as wrong as one smaller than it.
|
|
484
|
+
outside = sorted(set(owner) - set(changed_all))
|
|
485
|
+
if outside:
|
|
486
|
+
raise ManifestError(
|
|
487
|
+
"partitions cover changed paths outside the reviewed scope: " + ", ".join(outside[:5])
|
|
488
|
+
)
|
|
489
|
+
return per_partition
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def render_manifest(
|
|
493
|
+
module: types.ModuleType,
|
|
494
|
+
repo_root: Path,
|
|
495
|
+
base: str,
|
|
496
|
+
path_lists: list[list[str]],
|
|
497
|
+
excludes: tuple[str, ...],
|
|
498
|
+
changed_all: list[str],
|
|
499
|
+
) -> dict:
|
|
500
|
+
"""Build the manifest an author commits, with every hash computed by this gate."""
|
|
501
|
+
validated = [[validate_partition_path(value) for value in paths] for paths in path_lists]
|
|
502
|
+
seen: set[str] = set()
|
|
503
|
+
for paths in validated:
|
|
504
|
+
for value in paths:
|
|
505
|
+
if value in seen:
|
|
506
|
+
raise ManifestError(f"partition path is listed twice: {value}")
|
|
507
|
+
seen.add(value)
|
|
508
|
+
partition_coverage(repo_root, base, validated, excludes, changed_all)
|
|
509
|
+
partitions = [
|
|
510
|
+
{
|
|
511
|
+
"paths": paths,
|
|
512
|
+
"candidate_sha256": candidate_hash(module, repo_root, base, tuple(paths) + excludes),
|
|
513
|
+
}
|
|
514
|
+
for paths in validated
|
|
515
|
+
]
|
|
516
|
+
return {
|
|
517
|
+
"schema_version": MANIFEST_SCHEMA_VERSION,
|
|
518
|
+
"kind": MANIFEST_KIND,
|
|
519
|
+
"base": base,
|
|
520
|
+
"partitions": partitions,
|
|
521
|
+
"candidate_sha256": manifest_aggregate(base, partitions),
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def accepted_ledger_for(
|
|
526
|
+
evidence: list[tuple[Path, dict]],
|
|
527
|
+
repo_root: Path,
|
|
528
|
+
validator: Path,
|
|
529
|
+
digest: str,
|
|
530
|
+
rejected: list[str],
|
|
531
|
+
) -> str | None:
|
|
532
|
+
"""The first validator-accepted closeout ledger bound to `digest`, if any.
|
|
533
|
+
|
|
534
|
+
The same criterion the single-candidate path uses: a receipt-shaped file is
|
|
535
|
+
not evidence, only a ledger the validator accepts, because this gate cannot
|
|
536
|
+
authenticate that a controller minted what it reads.
|
|
537
|
+
"""
|
|
538
|
+
for path, payload in evidence:
|
|
539
|
+
if payload.get("candidate_sha256") != digest:
|
|
540
|
+
continue
|
|
541
|
+
if "closeout_state" not in payload or "controller_receipts" not in payload:
|
|
542
|
+
continue
|
|
543
|
+
relative = str(path.relative_to(repo_root))
|
|
544
|
+
accepted, output = validator_accepts(validator, path)
|
|
545
|
+
if accepted:
|
|
546
|
+
return f"{relative} -- {output}"
|
|
547
|
+
rejected.append(f"{relative}: {output}")
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def bind_manifest(
|
|
552
|
+
module: types.ModuleType,
|
|
553
|
+
repo_root: Path,
|
|
554
|
+
base: str,
|
|
555
|
+
payload: dict,
|
|
556
|
+
excludes: tuple[str, ...],
|
|
557
|
+
changed_all: list[str],
|
|
558
|
+
evidence: list[tuple[Path, dict]],
|
|
559
|
+
validator: Path,
|
|
560
|
+
rejected_ledgers: list[str],
|
|
561
|
+
) -> list[str]:
|
|
562
|
+
"""Bind the candidate through one manifest; return per-partition proof lines.
|
|
563
|
+
|
|
564
|
+
Raises ManifestError naming the first part that does not add up.
|
|
565
|
+
"""
|
|
566
|
+
path_lists, digests = parse_manifest(payload, base)
|
|
567
|
+
partition_coverage(repo_root, base, path_lists, excludes, changed_all)
|
|
568
|
+
proofs: list[str] = []
|
|
569
|
+
for index, (paths, recorded) in enumerate(zip(path_lists, digests), start=1):
|
|
570
|
+
label = f"partition {index} ({' '.join(paths)})"
|
|
571
|
+
actual = candidate_hash(module, repo_root, base, tuple(paths) + excludes)
|
|
572
|
+
if actual != recorded:
|
|
573
|
+
raise ManifestError(
|
|
574
|
+
f"{label} recorded {recorded[:12]}... but does not reproduce: the candidate now hashes to {actual[:12]}..."
|
|
575
|
+
)
|
|
576
|
+
proof = accepted_ledger_for(evidence, repo_root, validator, actual, rejected_ledgers)
|
|
577
|
+
if proof is None:
|
|
578
|
+
raise ManifestError(f"no accepted ledger binds {label} {actual}")
|
|
579
|
+
proofs.append(f" {label} {actual[:12]}... <- {proof}")
|
|
580
|
+
return proofs
|
|
581
|
+
|
|
582
|
+
|
|
315
583
|
def validator_accepts(validator: Path, ledger_path: Path) -> tuple[bool, str]:
|
|
316
584
|
result = subprocess.run(
|
|
317
585
|
[sys.executable, str(validator), str(ledger_path)],
|
|
@@ -382,7 +650,27 @@ def main() -> int:
|
|
|
382
650
|
action="store_true",
|
|
383
651
|
help="print the candidate hash the evidence must bind, then exit",
|
|
384
652
|
)
|
|
653
|
+
parser.add_argument(
|
|
654
|
+
"--print-manifest",
|
|
655
|
+
action="store_true",
|
|
656
|
+
help=(
|
|
657
|
+
"render a landing partition manifest for the --partition groups given, "
|
|
658
|
+
"with every hash computed by this gate, then exit"
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
parser.add_argument(
|
|
662
|
+
"--partition",
|
|
663
|
+
action="append",
|
|
664
|
+
nargs="+",
|
|
665
|
+
metavar="PATH",
|
|
666
|
+
default=[],
|
|
667
|
+
help="one partition's paths; repeat per partition (only with --print-manifest)",
|
|
668
|
+
)
|
|
385
669
|
args = parser.parse_args()
|
|
670
|
+
if args.print_manifest and not args.partition:
|
|
671
|
+
parser.error("--print-manifest needs at least one --partition")
|
|
672
|
+
if args.partition and not args.print_manifest:
|
|
673
|
+
parser.error("--partition is only meaningful with --print-manifest")
|
|
386
674
|
|
|
387
675
|
repo_root = Path(args.repo_root).resolve()
|
|
388
676
|
base = args.base or os.environ.get("CCL_SKILL_BASE_REF", "")
|
|
@@ -410,61 +698,114 @@ def main() -> int:
|
|
|
410
698
|
# confirmed, and no test asserts a bypass this gate does not have. `literal`
|
|
411
699
|
# stays because interpreting these names as patterns is a capability the gate
|
|
412
700
|
# never needed, and removing it costs nothing.
|
|
413
|
-
|
|
701
|
+
excludes = tuple(
|
|
414
702
|
f":(exclude,literal){path}" for path in added_evidence_paths(repo_root, base)
|
|
415
703
|
)
|
|
704
|
+
paths = tuple(args.paths) + excludes
|
|
416
705
|
require_committed_tree(repo_root, paths)
|
|
417
706
|
changed = changed_skill_paths(repo_root, base, paths)
|
|
418
707
|
if not changed:
|
|
419
708
|
# No reviewed path moved, so there is no candidate to freeze and nothing to
|
|
420
709
|
# bind. Say which it is rather than letting an empty packet surface as a
|
|
421
710
|
# freeze error, which reads like a broken gate.
|
|
422
|
-
if args.print_candidate:
|
|
711
|
+
if args.print_candidate or args.print_manifest:
|
|
423
712
|
emit(f"review_ledger_binding_no_change: no reviewed-path change against {base}")
|
|
424
713
|
else:
|
|
425
714
|
print(f"review_ledger_binding_ok: no reviewed-path change against {base}")
|
|
426
715
|
return 0
|
|
427
716
|
|
|
428
717
|
module = load_controller(repo_root)
|
|
718
|
+
|
|
719
|
+
if args.print_manifest:
|
|
720
|
+
try:
|
|
721
|
+
manifest = render_manifest(module, repo_root, base, args.partition, excludes, changed)
|
|
722
|
+
except ManifestError as exc:
|
|
723
|
+
emit(f"review_ledger_binding_error: cannot render a landing partition manifest: {exc}")
|
|
724
|
+
return 1
|
|
725
|
+
except Exception as exc: # noqa: BLE001 - surface the controller's own message
|
|
726
|
+
emit(f"review_ledger_binding_error: cannot freeze a partition packet: {exc}")
|
|
727
|
+
return 1
|
|
728
|
+
print(json.dumps(manifest, indent=2, ensure_ascii=False))
|
|
729
|
+
return 0
|
|
730
|
+
|
|
731
|
+
# The whole candidate may be larger than one packet. That is no longer a
|
|
732
|
+
# terminal error: record why the single freeze failed and let a committed
|
|
733
|
+
# partition manifest bind the candidate part by part.
|
|
734
|
+
expected: str | None = None
|
|
735
|
+
whole_error: str | None = None
|
|
429
736
|
try:
|
|
430
737
|
expected = candidate_hash(module, repo_root, base, paths)
|
|
431
738
|
except Exception as exc: # noqa: BLE001 - surface the controller's own message
|
|
432
|
-
|
|
433
|
-
return 1
|
|
739
|
+
whole_error = str(exc)
|
|
434
740
|
|
|
435
741
|
if args.print_candidate:
|
|
742
|
+
if expected is None:
|
|
743
|
+
emit(f"review_ledger_binding_error: cannot freeze the candidate packet: {whole_error}")
|
|
744
|
+
return 1
|
|
436
745
|
print(expected)
|
|
437
746
|
return 0
|
|
438
747
|
|
|
439
748
|
validator = repo_root / "skills" / "skill-extraction-workflow" / "scripts" / VALIDATOR
|
|
749
|
+
evidence = scan(repo_root, args.evidence_root)
|
|
440
750
|
ledgers: list[str] = []
|
|
441
|
-
|
|
442
|
-
if payload.get("candidate_sha256") != expected:
|
|
443
|
-
continue
|
|
444
|
-
relative = str(path.relative_to(repo_root))
|
|
751
|
+
if expected is not None:
|
|
445
752
|
# Only a validator-accepted ledger counts. A receipt-shaped file proves
|
|
446
753
|
# nothing on its own: this gate cannot authenticate that a controller
|
|
447
754
|
# minted it, so any branch keyed on a self-declared field is a bypass a
|
|
448
755
|
# contributor can hand-write.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
756
|
+
proof = accepted_ledger_for(evidence, repo_root, validator, expected, ledgers)
|
|
757
|
+
if proof is not None:
|
|
758
|
+
print(
|
|
759
|
+
f"review_ledger_binding_ok: {proof.split(' -- ', 1)[0]} binds the landing "
|
|
760
|
+
f"candidate ({expected[:12]}...) -- {proof.split(' -- ', 1)[1]}"
|
|
761
|
+
)
|
|
762
|
+
return 0
|
|
763
|
+
|
|
764
|
+
manifests: list[str] = []
|
|
765
|
+
for path, payload in evidence:
|
|
766
|
+
if payload.get("kind") != MANIFEST_KIND:
|
|
767
|
+
continue
|
|
768
|
+
relative = str(path.relative_to(repo_root))
|
|
769
|
+
try:
|
|
770
|
+
proofs = bind_manifest(
|
|
771
|
+
module, repo_root, base, payload, excludes, changed, evidence, validator, ledgers
|
|
772
|
+
)
|
|
773
|
+
except ManifestError as exc:
|
|
774
|
+
manifests.append(f"{relative}: {exc}")
|
|
775
|
+
continue
|
|
776
|
+
except Exception as exc: # noqa: BLE001 - surface the controller's own message
|
|
777
|
+
manifests.append(f"{relative}: cannot freeze a partition packet: {exc}")
|
|
778
|
+
continue
|
|
779
|
+
print(
|
|
780
|
+
f"review_ledger_binding_ok: {relative} binds the landing candidate as "
|
|
781
|
+
f"{len(proofs)} partitions (aggregate {payload['candidate_sha256'][:12]}...)"
|
|
782
|
+
)
|
|
783
|
+
for line in proofs:
|
|
784
|
+
print(line)
|
|
785
|
+
return 0
|
|
786
|
+
|
|
787
|
+
if expected is None:
|
|
788
|
+
emit(
|
|
789
|
+
"review_ledger_binding_failed: the whole candidate cannot be frozen as one "
|
|
790
|
+
f"packet ({whole_error}) and no committed landing partition manifest binds it"
|
|
791
|
+
)
|
|
792
|
+
emit(
|
|
793
|
+
" split the candidate by path: --print-manifest --partition <paths> "
|
|
794
|
+
"[--partition <paths> ...] renders the manifest; commit it with one "
|
|
795
|
+
"validated ledger per partition"
|
|
796
|
+
)
|
|
797
|
+
else:
|
|
798
|
+
emit(
|
|
799
|
+
"review_ledger_binding_failed: no accepted review evidence binds the landing "
|
|
800
|
+
f"candidate {expected}"
|
|
801
|
+
)
|
|
463
802
|
emit(f" reviewed paths: {' '.join(paths)} against {base}")
|
|
464
803
|
emit(f" changed files: {len(changed)}")
|
|
465
804
|
for row in ledgers:
|
|
466
805
|
emit(f" rejected ledger -> {row}")
|
|
467
|
-
|
|
806
|
+
for row in manifests:
|
|
807
|
+
emit(f" rejected manifest -> {row}")
|
|
808
|
+
if not ledgers and not manifests:
|
|
468
809
|
emit(
|
|
469
810
|
" no committed ledger records this candidate; run the extraction review "
|
|
470
811
|
"lane against the final, committed tree"
|
|
@@ -307,6 +307,191 @@ out="$(run_scope --base "$SCOPE_BASE" --print-candidate)"; rc=$?
|
|
|
307
307
|
check "the same clean checkout still reaches its normal verdict" \
|
|
308
308
|
'[ "$rc" = 0 ] && case "$out" in *"no reviewed-path change"*) true;; *) false;; esac'
|
|
309
309
|
|
|
310
|
+
# Partition: the candidate's Git identity is base..HEAD, but the packet a reviewer
|
|
311
|
+
# can read is capped at the controller's byte ceiling. A landing candidate larger
|
|
312
|
+
# than one packet used to be unlandable as one pull request -- the whole-candidate
|
|
313
|
+
# freeze failed and no ledger could ever bind it -- so authors split the pull
|
|
314
|
+
# request instead of the review. A committed landing partition manifest names
|
|
315
|
+
# path partitions that together cover every changed file exactly once, each bound
|
|
316
|
+
# by its own validated ledger; the gate recomputes every partition and refuses any
|
|
317
|
+
# manifest whose parts do not add up to the whole.
|
|
318
|
+
PART="$WORK/partition-repo"
|
|
319
|
+
mkdir -p "$PART/skills/code-review/scripts" "$PART/skills/skill-extraction-workflow/scripts" "$PART/specs/round/evidence" "$PART/lane-a" "$PART/lane-b"
|
|
320
|
+
cp "$CONTROLLER" "$PART/skills/code-review/scripts/review_gate.py"
|
|
321
|
+
cat >"$PART/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py" <<'PY'
|
|
322
|
+
import sys
|
|
323
|
+
print("extraction_review_state_ok: stub accepted")
|
|
324
|
+
sys.exit(0)
|
|
325
|
+
PY
|
|
326
|
+
git -C "$PART" init -q .
|
|
327
|
+
git -C "$PART" config user.email t@example.invalid
|
|
328
|
+
git -C "$PART" config user.name tester
|
|
329
|
+
printf 'baseline\n' >"$PART/skills/skill-extraction-workflow/SKILL.md"
|
|
330
|
+
printf 'baseline\n' >"$PART/lane-a/big.txt"
|
|
331
|
+
printf 'baseline\n' >"$PART/lane-b/big.txt"
|
|
332
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm baseline
|
|
333
|
+
PART_BASE="$(git -C "$PART" rev-parse HEAD)"
|
|
334
|
+
|
|
335
|
+
run_part() { python3 "$GATE" --repo-root "$PART" "$@" 2>&1; }
|
|
336
|
+
|
|
337
|
+
# The manifest oracle mirrors the documented canonical form (sorted keys, compact
|
|
338
|
+
# separators, UTF-8) independently of the gate, so a drift in either side reds.
|
|
339
|
+
cat >"$WORK/write-manifest.py" <<'PY'
|
|
340
|
+
import hashlib, json, sys
|
|
341
|
+
out, base, aggregate = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
342
|
+
partitions = []
|
|
343
|
+
for spec in sys.argv[4:]:
|
|
344
|
+
paths, digest = spec.rsplit("=", 1)
|
|
345
|
+
partitions.append({"paths": paths.split(), "candidate_sha256": digest})
|
|
346
|
+
body = {"schema_version": 1, "kind": "landing_partition_manifest", "base": base, "partitions": partitions}
|
|
347
|
+
if aggregate == "AUTO":
|
|
348
|
+
canonical = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
349
|
+
aggregate = hashlib.sha256(canonical).hexdigest()
|
|
350
|
+
body["candidate_sha256"] = aggregate
|
|
351
|
+
with open(out, "w", encoding="utf-8") as handle:
|
|
352
|
+
json.dump(body, handle, indent=2)
|
|
353
|
+
handle.write("\n")
|
|
354
|
+
PY
|
|
355
|
+
write_closeout() { python3 - "$1" "$2" <<'PY'
|
|
356
|
+
import json, sys
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "ready_for_human_decision", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
|
|
359
|
+
PY
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Two lanes each change ~130 KB: either fits one packet, the pair does not.
|
|
363
|
+
python3 - "$PART" <<'PY'
|
|
364
|
+
import sys
|
|
365
|
+
from pathlib import Path
|
|
366
|
+
root = Path(sys.argv[1])
|
|
367
|
+
for lane in ("lane-a", "lane-b"):
|
|
368
|
+
lines = [f"{lane} line {index:06d} " + "x" * 40 for index in range(2200)]
|
|
369
|
+
(root / lane / "big.txt").write_text("\n".join(lines) + "\n")
|
|
370
|
+
PY
|
|
371
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "oversized landing"
|
|
372
|
+
|
|
373
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
374
|
+
check "a candidate too large for one packet is refused with the partition recipe, not a bare freeze error" \
|
|
375
|
+
'[ "$rc" = 1 ] && case "$out" in *"exceeds 200000 bytes"*"landing partition manifest"*) true;; *) false;; esac'
|
|
376
|
+
|
|
377
|
+
MANIFEST_JSON="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-b)"; rc=$?
|
|
378
|
+
check "the gate renders a manifest for a partition that covers the whole candidate" \
|
|
379
|
+
'[ "$rc" = 0 ] && python3 -c "import json,sys; m=json.loads(sys.argv[1]); assert m[\"kind\"]==\"landing_partition_manifest\" and len(m[\"partitions\"])==2 and m[\"base\"]==sys.argv[2]" "$MANIFEST_JSON" "$PART_BASE"'
|
|
380
|
+
|
|
381
|
+
out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a)"; rc=$?
|
|
382
|
+
check "rendering refuses a partition that leaves a changed path uncovered" \
|
|
383
|
+
'[ "$rc" != 0 ] && case "$out" in *"uncovered"*"lane-b/big.txt"*) true;; *) false;; esac'
|
|
384
|
+
|
|
385
|
+
out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-a/big.txt lane-b)"; rc=$?
|
|
386
|
+
check "rendering refuses partitions that overlap on a changed path" \
|
|
387
|
+
'[ "$rc" != 0 ] && case "$out" in *"overlap"*"lane-a/big.txt"*) true;; *) false;; esac'
|
|
388
|
+
|
|
389
|
+
out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-a lane-b)"; rc=$?
|
|
390
|
+
check "rendering refuses the same path listed in two partitions before touching git" \
|
|
391
|
+
'[ "$rc" != 0 ] && case "$out" in *"listed twice"*"lane-a"*) true;; *) false;; esac'
|
|
392
|
+
|
|
393
|
+
HASH_A="$(run_part --base "$PART_BASE" --print-candidate --paths lane-a)"
|
|
394
|
+
HASH_B="$(run_part --base "$PART_BASE" --print-candidate --paths lane-b)"
|
|
395
|
+
check "a partition's hash is exactly what --print-candidate --paths already answers" \
|
|
396
|
+
'[ ${#HASH_A} = 64 ] && python3 -c "import json,sys; m=json.loads(sys.argv[1]); assert [p[\"candidate_sha256\"] for p in m[\"partitions\"]]==[sys.argv[2], sys.argv[3]]" "$MANIFEST_JSON" "$HASH_A" "$HASH_B"'
|
|
397
|
+
|
|
398
|
+
printf '%s\n' "$MANIFEST_JSON" >"$PART/specs/round/evidence/landing-partitions.json"
|
|
399
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "partition manifest"
|
|
400
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
401
|
+
check "a manifest whose partitions carry no validated ledger is refused, naming the partition" \
|
|
402
|
+
'[ "$rc" = 1 ] && case "$out" in *"no accepted ledger"*"lane-a"*) true;; *) false;; esac'
|
|
403
|
+
HASH_A_AFTER="$(run_part --base "$PART_BASE" --print-candidate --paths lane-a)"
|
|
404
|
+
check "committing the manifest does not move the partition it describes" \
|
|
405
|
+
'[ "$HASH_A_AFTER" = "$HASH_A" ]'
|
|
406
|
+
|
|
407
|
+
write_closeout "$PART/specs/round/evidence/closeout-a.json" "$HASH_A"
|
|
408
|
+
write_closeout "$PART/specs/round/evidence/closeout-b.json" "$HASH_B"
|
|
409
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "partition ledgers"
|
|
410
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
411
|
+
check "a complete manifest with a validated ledger per partition binds a candidate no single packet could" \
|
|
412
|
+
'[ "$rc" = 0 ] && case "$out" in *"binds the landing candidate"*"2 partitions"*) true;; *) false;; esac'
|
|
413
|
+
PART_GOOD="$(git -C "$PART" rev-parse HEAD)"
|
|
414
|
+
|
|
415
|
+
# Every way the parts can fail to add up to the whole is refused for its own
|
|
416
|
+
# reason. Each probe branches from the passing state so the cases stay independent.
|
|
417
|
+
MANIFEST="$PART/specs/round/evidence/landing-partitions.json"
|
|
418
|
+
probe_part() {
|
|
419
|
+
git -C "$PART" checkout -q -B "probe" "$PART_GOOD"
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
probe_part
|
|
423
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A"
|
|
424
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest drops a lane"
|
|
425
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
426
|
+
check "a manifest that leaves a changed path in no partition is refused" \
|
|
427
|
+
'[ "$rc" = 1 ] && case "$out" in *"uncovered"*"lane-b/big.txt"*) true;; *) false;; esac'
|
|
428
|
+
|
|
429
|
+
probe_part
|
|
430
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A" "lane-a/big.txt lane-b=$HASH_B"
|
|
431
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest overlaps"
|
|
432
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
433
|
+
check "a manifest whose partitions overlap is refused before any packet is frozen" \
|
|
434
|
+
'[ "$rc" = 1 ] && case "$out" in *"overlap"*"lane-a/big.txt"*) true;; *) false;; esac'
|
|
435
|
+
|
|
436
|
+
probe_part
|
|
437
|
+
printf 'moved after the manifest was written\n' >>"$PART/lane-a/big.txt"
|
|
438
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "candidate moves after manifest"
|
|
439
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
440
|
+
check "a partition whose recorded hash no longer reproduces is refused" \
|
|
441
|
+
'[ "$rc" = 1 ] && case "$out" in *"lane-a"*"does not reproduce"*) true;; *) false;; esac'
|
|
442
|
+
|
|
443
|
+
probe_part
|
|
444
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "0000000000000000000000000000000000000000" AUTO "lane-a=$HASH_A" "lane-b=$HASH_B"
|
|
445
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest names another base"
|
|
446
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
447
|
+
check "a manifest written against another base is refused" \
|
|
448
|
+
'[ "$rc" = 1 ] && case "$out" in *"base"*"fork point"*) true;; *) false;; esac'
|
|
449
|
+
|
|
450
|
+
probe_part
|
|
451
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" "$(printf 'f%.0s' $(seq 64))" "lane-a=$HASH_A" "lane-b=$HASH_B"
|
|
452
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest aggregate forged"
|
|
453
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
454
|
+
check "a manifest whose aggregate hash does not reproduce its partitions is refused" \
|
|
455
|
+
'[ "$rc" = 1 ] && case "$out" in *"aggregate"*) true;; *) false;; esac'
|
|
456
|
+
|
|
457
|
+
for bad_path in ":(exclude)lane-b" "-lane-b" "../lane-b" "/lane-b" "lane-*" "lane-?" "lane-[ab]" "lane-\\b"; do
|
|
458
|
+
probe_part
|
|
459
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A" "$bad_path=$HASH_B"
|
|
460
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest smuggles a pathspec"
|
|
461
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
462
|
+
check "a manifest partition path shaped like $bad_path is refused as a path, not handed to git" \
|
|
463
|
+
'[ "$rc" = 1 ] && case "$out" in *"partition path"*) true;; *) false;; esac'
|
|
464
|
+
done
|
|
465
|
+
|
|
466
|
+
out="$(run_part --base "$PART_BASE" --print-manifest --partition 'lane-*')"; rc=$?
|
|
467
|
+
check "rendering refuses a wildcard partition instead of letting git expand it" \
|
|
468
|
+
'[ "$rc" != 0 ] && case "$out" in *"partition path"*"wildcard"*) true;; *) false;; esac'
|
|
469
|
+
|
|
470
|
+
# Under a narrowed --paths scope the partition union must equal the reviewed
|
|
471
|
+
# changed set, not merely contain it: a partition naming `.` reaches lane-b even
|
|
472
|
+
# though only lane-a is under review, and that surplus is refused before any
|
|
473
|
+
# packet is frozen. The ledgers are removed so the single-ledger path cannot
|
|
474
|
+
# satisfy the narrowed scope first.
|
|
475
|
+
probe_part
|
|
476
|
+
git -C "$PART" rm -q "$PART/specs/round/evidence/closeout-a.json" "$PART/specs/round/evidence/closeout-b.json"
|
|
477
|
+
python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO ".=$(printf '0%.0s' $(seq 64))"
|
|
478
|
+
git -C "$PART" add -A && git -C "$PART" commit -qm "manifest reaches outside the reviewed scope"
|
|
479
|
+
out="$(run_part --base "$PART_BASE" --paths lane-a)"; rc=$?
|
|
480
|
+
check "a partition reaching changed paths outside a narrowed --paths scope is refused, not accepted as covering" \
|
|
481
|
+
'[ "$rc" = 1 ] && case "$out" in *"outside the reviewed scope"*"lane-b/big.txt"*) true;; *) false;; esac'
|
|
482
|
+
|
|
483
|
+
probe_part
|
|
484
|
+
git -C "$PART" rm -q "$PART/specs/round/evidence/closeout-b.json"
|
|
485
|
+
git -C "$PART" commit -qm "one ledger missing"
|
|
486
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
487
|
+
check "a manifest with one partition unbound is refused, naming that partition" \
|
|
488
|
+
'[ "$rc" = 1 ] && case "$out" in *"no accepted ledger"*"lane-b"*) true;; *) false;; esac'
|
|
489
|
+
|
|
490
|
+
git -C "$PART" checkout -q -B "landing" "$PART_GOOD"
|
|
491
|
+
out="$(run_part --base "$PART_BASE")"; rc=$?
|
|
492
|
+
check "the passing partition state still passes after the probes" \
|
|
493
|
+
'[ "$rc" = 0 ]'
|
|
494
|
+
|
|
310
495
|
# The suite above exercises a synthetic repository. The gate must also run against
|
|
311
496
|
# the real checkout it ships in, or a break in that path passes every test here.
|
|
312
497
|
REAL_ROOT="$(cd "$DIR/../../.." && pwd -P)"
|