Skip to content

Commit 7d85328

Browse files
yyodsclaude
andcommitted
[green] bind completion era to candidate source ancestry
Turn the completion-path ancestry RED green without weakening anything. The gap: create_hil_completion_fragment selected the ADR-0038 derivation era only through the qualification checkout's HEAD (_load_qualification_policy -> _qualification_derivation_for_checkout) and never consulted the CANDIDATE's bound source commit; on a root without git identity the era fell back silently to the historical version-routed arithmetic inside _derived_qualification_thresholds, surfacing as the misleading 'OI-1 policy thresholds were not derived from baseline esp32-4mb' error the repaired fixture uncovered. Fix (firmware/scripts/release_bundle.py): - create_hil_completion_fragment derives the era once from release.provenance.pyble.commit via _qualification_derivation_for_source against the qualification root (strict descendant of 5620f2f => V4; boundary/ancestor => historical; unprovable => fail closed), and threads it through both _load_v5_completion_observation calls. - _load_v5_completion_observation requires the candidate-bound derivation and passes it into _load_qualification_policy; the lineage branch keeps its own clean-checkout re-derivation. - _load_qualification_policy accepts an optional pre-proven derivation and forwards it to _validate_qualification_policy; every other call site is unchanged (HEAD-ancestry routing as before). - _qualification_derivation_for_source now names ancestry when failing closed: a merge-base failure (missing object, non-checkout, unrelated history) raises 'ancestry ... unprovable' instead of the bare cannot-inspect message; the diverged-sibling refusal keeps its fail-closed behaviour with the same ancestry wording. No acceptance is widened anywhere. Witnessed green: test_release_v060_promotion_lifecycle (7 OK, including the new ancestry pin), test_release_v060_contract (12 OK), test_release_v060_lifecycle (13 OK), test_release_historical_source_contract (15 OK), test_oi1_profile_bench (78 OK), test_oi1_rp2_profile_bench (15 OK), test_release_finalization (21 OK), test_v060_qualification_workflow (19 OK), test_release_v060_private_gates (18 OK). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CAVghCmvKjF4BZFBUjAMwc Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent d1aca40 commit 7d85328

1 file changed

Lines changed: 58 additions & 12 deletions

File tree

‎firmware/scripts/release_bundle.py‎

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20991,13 +20991,20 @@ def _qualification_derivation_for_source(
2099120991
and COMMIT_RE.fullmatch(source_commit) is not None,
2099220992
"qualification source commit must be full lowercase 40-hex",
2099320993
)
20994-
merge_base = _git_output(
20995-
Path(checkout),
20996-
"PyBLE",
20997-
"merge-base",
20998-
V060_SUPERSEDED_SOURCE_BOUNDARY,
20999-
source_commit,
21000-
)
20994+
try:
20995+
merge_base = _git_output(
20996+
Path(checkout),
20997+
"PyBLE",
20998+
"merge-base",
20999+
V060_SUPERSEDED_SOURCE_BOUNDARY,
21000+
source_commit,
21001+
)
21002+
except ReleaseError as exc:
21003+
raise ReleaseError(
21004+
"qualification source %s ancestry against the superseded "
21005+
"v0.6.0 source boundary is unprovable; failing closed instead "
21006+
"of guessing a derivation era" % source_commit
21007+
) from exc
2100121008
if (
2100221009
merge_base == V060_SUPERSEDED_SOURCE_BOUNDARY
2100321010
and source_commit != V060_SUPERSEDED_SOURCE_BOUNDARY
@@ -21006,8 +21013,9 @@ def _qualification_derivation_for_source(
2100621013
if merge_base == source_commit:
2100721014
return _qualification_derivation_for_version(firmware_version)
2100821015
raise ReleaseError(
21009-
"qualification source %s is unrelated to the superseded v0.6.0 "
21010-
"source boundary; the derivation era is unprovable" % source_commit
21016+
"qualification source %s ancestry is unrelated to the superseded "
21017+
"v0.6.0 source boundary; the derivation era is unprovable"
21018+
% source_commit
2101121019
)
2101221020

2101321021

@@ -21326,7 +21334,16 @@ def _validate_qualification_policy(
2132621334

2132721335
def _load_qualification_policy(
2132821336
repo_root: Path,
21337+
*,
21338+
derivation: dict[str, str] | None = None,
2132921339
) -> tuple[dict[str, Any], str]:
21340+
"""Load and fully revalidate the committed OI-1 qualification policy.
21341+
21342+
``derivation`` optionally binds the ADR-0038 era already proven from a
21343+
bound source commit's ancestry; without it a v0.6.0-or-later era is
21344+
routed from this checkout's own HEAD ancestry.
21345+
"""
21346+
2133021347
root = Path(repo_root)
2133121348
firmware_version = _read_lock(root)["pyble"]["agent_version"]
2133221349
path = _audit_repo_file(
@@ -21346,6 +21363,7 @@ def _load_qualification_policy(
2134621363
document,
2134721364
repo_root=root,
2134821365
firmware_version=firmware_version,
21366+
derivation=derivation,
2134921367
),
2135021368
_sha256_bytes(source),
2135121369
)
@@ -26237,7 +26255,17 @@ def _load_v5_completion_observation(
2623726255
profile_id: str,
2623826256
qualification_repo_root: Path,
2623926257
policy: dict[str, Any],
26258+
derivation: dict[str, str],
2624026259
) -> tuple[dict[str, Any], tuple[Any, ...]]:
26260+
"""Load one V5 completion observation under the candidate-bound era.
26261+
26262+
``derivation`` is the ADR-0038 era already proven from the CANDIDATE's
26263+
bound source commit's git ancestry; the committed policy is revalidated
26264+
under exactly that era, never under this checkout's HEAD or a SemVer
26265+
fallback. The lineage branch re-derives its receipt against a clean git
26266+
checkout, which re-proves the same binding on its own.
26267+
"""
26268+
2624126269
value, snapshot = _read_canonical_json_object(
2624226270
Path(path), "V5 OI-1 verify observation"
2624326271
)
@@ -26255,9 +26283,10 @@ def _load_v5_completion_observation(
2625526283
)
2625626284
baseline_path = (
2625726285
Path(qualification_repo_root)
26258-
/ _load_qualification_policy(Path(qualification_repo_root))[0][
26259-
"baseline_evidence"
26260-
]["path"]
26286+
/ _load_qualification_policy(
26287+
Path(qualification_repo_root),
26288+
derivation=derivation,
26289+
)[0]["baseline_evidence"]["path"]
2626126290
)
2626226291
baseline = _read_json(baseline_path, "active OI-1 baseline evidence")
2626326292
baseline_profile = next(
@@ -26592,6 +26621,21 @@ def create_hil_completion_fragment(
2659226621
== candidate_snapshot,
2659326622
"candidate release changed during HIL completion validation",
2659426623
)
26624+
# ADR-0038: the qualification-policy derivation era is a git-ancestry
26625+
# fact of the CANDIDATE's bound source commit — never of this checkout's
26626+
# HEAD and never of SemVer alone. Route by that commit's ancestry and
26627+
# fail closed when it is unprovable.
26628+
try:
26629+
candidate_source_commit = release["provenance"]["pyble"]["commit"]
26630+
except (KeyError, TypeError) as exc:
26631+
raise ReleaseError(
26632+
"candidate release provenance lacks its bound pyble source commit"
26633+
) from exc
26634+
completion_derivation = _qualification_derivation_for_source(
26635+
qualification_root,
26636+
candidate_source_commit,
26637+
firmware_version="0.6.0",
26638+
)
2659526639
try:
2659626640
pending_report_text = pending_report_raw.decode("utf-8", errors="strict")
2659726641
except UnicodeDecodeError as exc:
@@ -26657,6 +26701,7 @@ def create_hil_completion_fragment(
2665726701
profile_id=profile_id,
2665826702
qualification_repo_root=qualification_root,
2665926703
policy=policy_by_id[profile_id],
26704+
derivation=completion_derivation,
2666026705
)
2666126706

2666226707
candidate_release_digest = hashlib.sha256(release_raw).hexdigest()
@@ -26806,6 +26851,7 @@ def unchanged() -> None:
2680626851
profile_id=profile_id,
2680726852
qualification_repo_root=qualification_root,
2680826853
policy=policy_by_id[profile_id],
26854+
derivation=completion_derivation,
2680926855
)
2681026856
)
2681126857
_require(

0 commit comments

Comments
 (0)