@@ -160,7 +160,17 @@ class ReleaseError(RuntimeError):
160160 "boot_ceiling": "fixed-profile-product-slo-esp3000-pico7000-v4",
161161 "goodput_floor": "fixed-product-slo-64k-under-10s-6600-v3",
162162}
163- QUALIFICATION_DERIVATION = QUALIFICATION_DERIVATION_V4
163+ # ADR-0039: the second-replacement v0.6.0 derivation changes exactly the
164+ # heap-floor identifier. Every heap floor keeps floor-min-1024-v1
165+ # arithmetic except the fixed Waveshare largest-block floor.
166+ QUALIFICATION_DERIVATION_V5 = {
167+ "application_image": "exact-byte-identical-two-root-v1",
168+ "application_headroom": "factory-minus-application-v1",
169+ "heap_floor": "floor-min-1024-waveshare-block-98304-v2",
170+ "boot_ceiling": "fixed-profile-product-slo-esp3000-pico7000-v4",
171+ "goodput_floor": "fixed-product-slo-64k-under-10s-6600-v3",
172+ }
173+ QUALIFICATION_DERIVATION = QUALIFICATION_DERIVATION_V5
164174# ADR-0038: the superseded unpublished v0.6.0 source era ends at and
165175# includes this commit. Only strict descendants belong to the replacement
166176# v0.6.0 era and use QUALIFICATION_DERIVATION_V4; the boundary and its
@@ -174,10 +184,20 @@ class ReleaseError(RuntimeError):
174184V060_ABANDONED_CANDIDATE_SOURCE = (
175185 "719b211345028e49aee9df9b11c4b5fd110913de"
176186)
187+ # ADR-0039: the first-replacement candidate source is the second era
188+ # boundary. Only strict descendants use QUALIFICATION_DERIVATION_V5; the
189+ # boundary itself and its ancestors keep the ADR-0038 routing above.
190+ V060_FIRST_REPLACEMENT_SOURCE_BOUNDARY = (
191+ "7d853289815751c7381c9fd0b9a9a4409bdb6879"
192+ )
177193# ADR-0037 fixed product SLOs for the replacement v0.6.0 era.
178194V4_FIXED_RESET_SLO_MS_ESP = 3000
179195V4_FIXED_RESET_SLO_MS_RP2 = 7000
180196V4_FIXED_GOODPUT_FLOOR_BPS = 6600
197+ # ADR-0039: the fixed Waveshare largest-block floor — the baseline-derived
198+ # 102400 minus exactly one 4096-byte page.
199+ V5_FIXED_WAVESHARE_LARGEST_BLOCK_MIN_BYTES = 98304
200+ V5_WAVESHARE_PROFILE_ID = "waveshare-esp32-s3-lcd-147b"
181201QUALIFICATION_THRESHOLD_KEYS = (
182202 "application_image_max_bytes",
183203 "application_headroom_min_bytes",
@@ -20991,6 +21011,26 @@ def _qualification_derivation_for_source(
2099121011 and COMMIT_RE.fullmatch(source_commit) is not None,
2099221012 "qualification source commit must be full lowercase 40-hex",
2099321013 )
21014+ # ADR-0039: a strict descendant of the first-replacement boundary is
21015+ # the second-replacement era. When this stage cannot be proven (for
21016+ # example a historical checkout that predates the boundary object),
21017+ # fall through to the ADR-0038 routing below, which itself fails
21018+ # closed on any ancestry it cannot prove.
21019+ try:
21020+ second_merge_base = _git_output(
21021+ Path(checkout),
21022+ "PyBLE",
21023+ "merge-base",
21024+ V060_FIRST_REPLACEMENT_SOURCE_BOUNDARY,
21025+ source_commit,
21026+ )
21027+ except ReleaseError:
21028+ second_merge_base = None
21029+ if (
21030+ second_merge_base == V060_FIRST_REPLACEMENT_SOURCE_BOUNDARY
21031+ and source_commit != V060_FIRST_REPLACEMENT_SOURCE_BOUNDARY
21032+ ):
21033+ return QUALIFICATION_DERIVATION_V5
2099421034 try:
2099521035 merge_base = _git_output(
2099621036 Path(checkout),
@@ -21163,12 +21203,16 @@ def _validate_qualification_policy(
2116321203 firmware_version,
2116421204 "firmware source version",
2116521205 )
21166- # ADR-0038: SemVer alone cannot separate the two v0.6.0 source
21167- # eras. Without a bound source checkout both era derivations are
21168- # shape-admissible; identity-binding flows narrow to the exact era
21169- # through the source checkout's ancestry.
21206+ # ADR-0038/ADR-0039 : SemVer alone cannot separate the v0.6.0
21207+ # source eras. Without a bound source checkout every era
21208+ # derivation is shape-admissible; identity-binding flows narrow to
21209+ # the exact era through the source checkout's ancestry.
2117021210 allowed_derivations = (
21171- (QUALIFICATION_DERIVATION_V3, QUALIFICATION_DERIVATION_V4)
21211+ (
21212+ QUALIFICATION_DERIVATION_V3,
21213+ QUALIFICATION_DERIVATION_V4,
21214+ QUALIFICATION_DERIVATION_V5,
21215+ )
2117221216 if selected == QUALIFICATION_DERIVATION_V3
2117321217 and source_core >= (0, 6, 0)
2117421218 else (selected,)
@@ -21179,6 +21223,7 @@ def _validate_qualification_policy(
2117921223 QUALIFICATION_DERIVATION_V2,
2118021224 QUALIFICATION_DERIVATION_V3,
2118121225 QUALIFICATION_DERIVATION_V4,
21226+ QUALIFICATION_DERIVATION_V5,
2118221227 )
2118321228 _require(
2118421229 all(type(value) is str for value in derivation.values())
@@ -22123,7 +22168,10 @@ def floor_min(metric: str, quantum: int) -> int:
2212322168 ):
2212422169 put_min = (put_min * 95) // 100
2212522170 get_min = (get_min * 95) // 100
22126- if derivation == QUALIFICATION_DERIVATION_V4:
22171+ if derivation in (
22172+ QUALIFICATION_DERIVATION_V4,
22173+ QUALIFICATION_DERIVATION_V5,
22174+ ):
2212722175 # ADR-0037: fixed product SLOs, never functions of baseline extrema.
2212822176 reset_threshold = (
2212922177 V4_FIXED_RESET_SLO_MS_RP2
@@ -22179,6 +22227,15 @@ def floor_min(metric: str, quantum: int) -> int:
2217922227 ),
2218022228 }
2218122229 )
22230+ if (
22231+ derivation == QUALIFICATION_DERIVATION_V5
22232+ and profile_id == V5_WAVESHARE_PROFILE_ID
22233+ ):
22234+ # ADR-0039: the single fixed heap-floor exception — never a
22235+ # function of the observed samples.
22236+ derived["idf_internal_largest_block_min_bytes"] = (
22237+ V5_FIXED_WAVESHARE_LARGEST_BLOCK_MIN_BYTES
22238+ )
2218222239 for key, value in derived.items():
2218322240 _qualification_integer(
2218422241 value,
@@ -24375,7 +24432,10 @@ def _profile_thresholds(
2437524432 observation: dict[str, Any],
2437624433 profile_id: str,
2437724434 ) -> dict[str, int]:
24378- if qualification_derivation == QUALIFICATION_DERIVATION_V4:
24435+ if qualification_derivation in (
24436+ QUALIFICATION_DERIVATION_V4,
24437+ QUALIFICATION_DERIVATION_V5,
24438+ ):
2437924439 return _derived_qualification_thresholds(
2438024440 build,
2438124441 observation,
0 commit comments