Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.d/20261001-inherited-quality-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### 검증·품질 게이트

- 상속된 CI helper의 CLI·오류·경계 테스트와 docstring 누락을 복구했다. Coverage 기준은 유지한다.
- 릴리즈 runtime 대상 집합을 파일 접근 전에 검증하고, 다운로드 응답의 오류 경로에서도 자원을 정리한다.
- Base와 이 수리의 로컬 전체 측정은 5,274 tests passed, statement·branch coverage 100%, docstring 100%다. 최종 결합 HEAD와 hosted 수용은 별도 검증 대상이다.
- 원인·RED/GREEN·검증 경계: [상속 품질 게이트 복구 기록](../docs/doctoring/inherited-quality-gates-20261001.md).
3 changes: 3 additions & 0 deletions CHANGELOG.d/20261004-noema-npm-semver-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### 보안 검증

- Noema document-reader의 npm lock 보안 floor가 PEP 440 대신 npm SemVer 우선순위를 사용하도록 고쳤다. 숫자 prerelease는 같은 final release보다 낮게 처리한다.
45 changes: 45 additions & 0 deletions docs/doctoring/inherited-quality-gates-20261001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 상속된 coverage·docstring 게이트 복구

## 원인과 범위

PR #2261의 정확한 base `7900ba4c4d68c378023592252f2579646fad9aaa`를 별도 clean worktree에서 검증했다. 전체 테스트는 5,182개 통과했지만 coverage는 99%(미측정 statement 181개), docstring은 97.1%였다. PR head `0852d3044a09a5c751589c3d2024293cb99b6ff8`의 공유 77개 모듈별 coverage 결과는 base와 같았다. 원인 귀속을 분리하는 근거이며, 실패한 게이트를 면제하는 근거는 아니다.

누락은 세 가지가 섞인 결과였다. 일부 테스트는 coverage가 시작되지 않은 child Python에서 CLI를 실행했다. 다른 테스트는 CLI·오류 경로를 호출하지 않았다. 선행 검증이 이미 강제한 불변식을 마지막에 다시 검사하는 중복 조건도 있었다.

기존 테스트에 실제 CLI 결과, 잘못된 입력의 거부, 오류의 원인 보존, 자원 정리, queued-only 변경 경계를 검증하는 assertion을 추가했다. 기존 subprocess smoke는 유지하며 in-process 검증을 보충했다. 누락된 docstring은 현재 함수의 입력·출력·실패 계약에 맞춰 복구했다. Coverage 설정·threshold·dependency pin은 바꾸지 않았다.

## 보안 경계

- Runtime archive prescreen은 정확한 primary 13개·Intel variant 3개 대상 집합을 파일 및 native 검사 전에 확인한다. 누락·중복·대체 대상·잘못된 variant는 `SCOPE_UNVERIFIABLE`로 거부한다. 각 archive의 비어 있지 않음, byte identity와 inventory 검증은 유지했다.
- Maturin 다운로드는 응답을 얻은 뒤에만 정리 블록에 들어가며 응답을 반드시 닫는다. HTTP/OSError의 취득·읽기 실패, non-200 응답, size 초과의 결과와 자원 정리를 검증했다. Proxy 차단, exact redirect, 허용 asset, 크기 제한은 유지했다.
- Pingora에서는 앞선 encoding 검사에 지배되는 중복 조건만 제거했다. 기존 encoding·content·raw transport 실패 검증은 유지했다.
- Cargo의 `file://` fixture는 두 dependency가 package-only 로컬 Git crate라는 것을 검증한다. 해당 lockfile 생성 호출에만 `CARGO_NET_OFFLINE=false`를 적용하며 이후 vendor·build는 offline 상태를 유지한다. 원격 registry 접근을 허용하는 변경이 아니다.

Air 담당 5개 파일과 선행 #2531의 원격 branch는 변경하지 않았다. 기존 repair 커밋의 테스트·docstring만 선택적으로 재사용했으며 parser dependency 변경이나 완전성 검사의 단순 삭제는 승계하지 않았다. Python 3.10용 선택적 `tomli` 테스트의 기존 `importorskip` 계약도 보존했다.

## 실제 검증

최초 offline 전체 실행은 5,258 passed와 1 failed였다. 전역 offline 설정이 최초 로컬 Git fetch도 차단한 fixture 실패이며 성공으로 재기록하지 않았다.

Base와 첫 수리 `4743e7f8c`의 측정은 5,274 passed, 4 skipped, 40 subtests passed였다. Statement 18,256개·branch 7,500개에서 누락 및 partial 0, coverage 100%, docstring 100%를 확인했다. 이 결과는 PR #2261의 최종 combined HEAD나 hosted acceptance를 증명하지 않는다. 결합 후 전체 게이트를 다시 실행하고 정확한 현재 HEAD의 필수 검사와 비작성자 formal approval을 확인해야 한다.

실행 명령:

```bash
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 GITHUB_ACTIONS=true CARGO_NET_OFFLINE=true \
python3 -m coverage run -m pytest tests -q
python3 -m coverage report --show-missing
python3 -m interrogate
```

독립 검토·현재 HEAD의 hosted 검사·선행 PR의 보호 main 통합·정식 head-guarded 병합이 완료되기 전에는 릴리즈 수용이나 merge authorization으로 취급하지 않는다.

## 2026-10-02 후속 오류 정리 검증

정적 검토에서 Maturin의 `read()`가 별도 HTTP 오류 stream을 만든 뒤 정상 response의 `close()`도 `OSError`를 내는 조합이 제기됐다. Production 발생을 확인한 것은 아니지만, 해당 조합의 합성 테스트는 실제로 stream이 열려 있음을 확인하며 실패했다. HTTP 오류 stream을 먼저 정리한 뒤 response 정리를 수행하도록 순서를 보강했다. 최종 cleanup 오류는 숨기지 않으며 원 HTTP 오류를 exception context에 보존한다.

새 회귀를 포함한 Maturin 테스트 24개, 해당 모듈의 statement·branch coverage 및 docstring 100%를 확인했다. 변경된 combined HEAD의 전체 재측정과 hosted 검증은 별도 진행한다.

이후 전체 실행은 5,312 passed와 1 failed였다. 실패는 기존 OpenCode CLI integration의 bare-origin 회귀에서 요청 경로가 `/chat/completions`로 기록됐지만 formatted 출력에 `not found`가 없었던 경우다. 설치된 CLI 1.18.33이 지원하는 `--format json`으로 결과를 수집하고 실제 `error` event 안의 같은 메시지를 확인하도록 보강했다. Gateway의 served-route·잘못된 경로·오류 메시지 assertion을 제거하지 않았으며 테스트용 timeout도 늘리지 않았다. 두 integration 테스트가 두 차례 통과했다(27.71초, structured error assertion 추가 후 30.49초). 이는 loopback stub을 대상으로 한 CLI 증거이며 실제 모델 추론이나 hosted approval은 아니다.

JSON 변경 후에도 전체 실행에서 같은 native CLI 실패가 재현됐다(5,312 passed / 1 failed). 이를 최종 복구로 취급하지 않았다. Helper는 첫 요청 직후 수집을 끝내고 20초 grace 뒤 CLI를 종료할 수 있어, 요청보다 늦게 도착하는 오류 event를 잃는 구조였다. 25초에 오류가 준비되는 simulated process는 기존 helper에서 premature kill로 RED였다. 오류 증거에는 CLI 종료까지 기다리고, 경로만 필요한 성공 사례에는 명시적으로 조기 종료하도록 분리한 뒤 GREEN을 확인했다. 기존 120초·20초 테스트 제한 수치는 바꾸지 않았다. Deterministic 회귀와 실제 native CLI 두 사례는 3 passed(69.47초)였다. 전체 결합 HEAD의 재측정은 별도 필요하다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Noema document-reader npm SemVer 보안 floor

## 관측

`tests/test_noema_document_reader_dependency_security.py`는 npm `package-lock.json`의 `fast-uri`와 `ip-address` 버전을 Python PEP 440 `packaging.version.Version`으로 비교했다. 이 비교는 npm SemVer와 prerelease 표기가 다르다.

고정된 부모 source `b2f1ab72788639aaf8389228606e9d70217336de`에서 `_locked_versions()`를 직접 실행하면 `3.1.8-1`은 `3.1.8.post1`으로 정규화되어 final security floor `3.1.8` 이상으로 허용됐다. 설치된 npm bundled semver 7.8.5는 같은 문자열을 유효한 numeric prerelease로 인식하고 final보다 낮게 판정했다. `3.1.8-beta.1`은 양쪽에서 낮고 `3.1.8`은 양쪽에서 final이다. 이 기록은 실제 lock에 취약한 prerelease가 있다는 주장이나 exploit 증거가 아니다.

## 결정

외부 Python version parser 또는 npm binary에 의존하지 않고, 테스트 파일 안에 strict npm SemVer parser를 둔다. parser는 `major.minor.patch`, prerelease identifier의 numeric-vs-string 우선순위, build metadata 무시, malformed value 거부를 구현한다. 이 검사는 document-reader package-lock 보안 floor 계약의 test oracle이며 runtime code가 아니다.

## 검증

RED는 `3.1.8-1 < 3.1.8`을 요구했으나 기존 PEP 440 helper가 이를 반대로 처리해 실패했다. GREEN은 numeric prerelease, named prerelease, build metadata, malformed version, nested vulnerable dependency 및 현재 lock floor를 포함한 focused 11 tests와 docstring 100%다.

현재 branch·hosted check·독립 approval 수용은 별도다. 부모 수정은 ordinary merge로 child stack에만 보존했고 원격 부모 branch를 직접 갱신하지 않았다.
1 change: 1 addition & 0 deletions scripts/ci/collect_release_strix_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def collect_bindings(


def main() -> None:
"""Collect artifact-bound Strix release receipts from command-line inputs."""
parser = argparse.ArgumentParser()
for name in (
"capture", "license-report", "plan", "metadata", "attempt", "repository",
Expand Down
7 changes: 7 additions & 0 deletions scripts/ci/opencode_queue_priority.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

@dataclass(frozen=True)
class QueuedRun:
"""One queued review run bound to its target PR and head."""
run_id: int
created_at: datetime
repo: str
Expand All @@ -47,13 +48,15 @@ class QueuedRun:

@dataclass(frozen=True)
class PrState:
"""Live PR identity and maintainer-attested priority."""
state: str
head: str
priority: bool


@dataclass(frozen=True)
class Plan:
"""Immutable keep and cancellation groups with queue ordering."""
keep: tuple[QueuedRun, ...]
cancel_current: tuple[QueuedRun, ...]
cancel_stale: tuple[QueuedRun, ...]
Expand Down Expand Up @@ -94,10 +97,12 @@ def metrics(p: Plan, *, now: datetime) -> dict:


def _gh(*args: str, stdin: str | None = None) -> str:
"""Run one GitHub CLI command and propagate command failures."""
return subprocess.run(["gh", *args], input=stdin, capture_output=True, text=True, check=True).stdout


def fetch_queued() -> list[QueuedRun]:
"""Parse queued review runs carrying the expected target identity."""
out = _gh("api", "--paginate", f"repos/{CENTRAL}/actions/workflows/{WORKFLOW}/runs?status=queued&per_page=100",
"--jq", ".workflow_runs[]|[.id,.created_at,.display_title]|@json")
runs = []
Expand All @@ -111,6 +116,7 @@ def fetch_queued() -> list[QueuedRun]:


def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int], PrState]:
"""Read live PR states and cache maintainer label permissions."""
live: dict[tuple[str, int], PrState] = {}
perms: dict[tuple[str, str], str] = {}
keys_sorted = sorted(keys)
Expand Down Expand Up @@ -144,6 +150,7 @@ def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int],


def main(argv: list[str] | None = None) -> int:
"""Report the plan and apply only recorded, still-queued cancellations."""
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--label", default="review-priority")
ap.add_argument("--include-current", action="store_true", help="also cancel non-priority current-head runs")
Expand Down
2 changes: 0 additions & 2 deletions scripts/ci/pingora_edge_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,8 +614,6 @@ def _load_raw_file_bytes(
return raw
if encoding == "none":
raise PolicyError(f"GitHub content evidence for {path} has no inline content and no verifiable oversized size")
if encoding != "base64":
raise PolicyError(f"GitHub content evidence for {path} is not a regular base64 file")
encoded = payload.get("content")
if not isinstance(encoded, str):
raise PolicyError(f"GitHub content evidence for {path} has a malformed size or content field")
Expand Down
3 changes: 3 additions & 0 deletions scripts/ci/place_maturin_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@


def _inside(path: pathlib.Path, root: pathlib.Path) -> bool:
"""Return whether a resolved path stays within the expected root."""
return path == root or root in path.parents


def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]:
"""Copy only native wheel members into existing source package directories."""
project = project_dir.resolve()
pyproject = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8"))
python_source = pyproject.get("tool", {}).get("maturin", {}).get("python-source", ".")
Expand All @@ -48,6 +50,7 @@ def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]:


def main(argv: list[str]) -> int:
"""Place the requested wheel or return a bounded usage or validation error."""
if len(argv) != 2:
print("usage: place_maturin_extension.py WHEEL PROJECT_DIR", file=sys.stderr)
return 2
Expand Down
32 changes: 25 additions & 7 deletions scripts/ci/prescreen_release_runtime_archives.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def _build_packages(item: Mapping[str, Any], folder: Path) -> list[dict[str, Any
raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} metadata is ambiguous")
metadata_root = PurePosixPath(metadata[0]).parent
def read_file(path: str) -> bytes:
"""Read one size-bounded package text member from the captured archive."""
entry = members.get(f"{name}/{path}")
if entry is None or entry.file_size > 4 * 1024 * 1024:
raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} text file is missing or oversized")
Expand Down Expand Up @@ -282,6 +283,26 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]:
or len(scope["verified_scope_evidence"]) != 13
or not isinstance(variants, list) or len(variants) != 3):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "verified scope evidence is incomplete")
versions = ("3.12", "3.13", "3.14")
expected_legs = {f"{target}-py{version}" for target in TARGET_ARCHES for version in versions} | {"sdist"}
expected_variants = {f"universal2-apple-darwin-py{version}" for version in versions}
for items, expected, variant in (
(scope["verified_scope_evidence"], expected_legs, False),
(variants, expected_variants, True),
):
if any(not isinstance(item, Mapping) or not isinstance(item.get("leg"), str)
or not re.fullmatch(r"[A-Za-z0-9_.+-]+", item["leg"])
or item["leg"] in {".", ".."} for item in items):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "scope evidence row is malformed")
legs = [item["leg"] for item in items]
if len(set(legs)) != len(legs):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "scope evidence row is malformed")
if set(legs) != expected:
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete")
if variant and any(item.get("arch") != "x86_64"
or item.get("artifact_name") != f"repro-macos-x86-{item['leg']}"
or not isinstance(item.get("archives"), list) for item in items):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "scope evidence row is malformed")
rows: dict[tuple[str, str], dict[str, Any]] = {}
build_rows: dict[str, dict[str, Any]] = {}
tool_rows: dict[str, dict[str, Any]] = {}
Expand All @@ -300,8 +321,6 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]:
or not isinstance(item.get("archives"), list)):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "scope evidence row is malformed")
leg = item["leg"]
if leg != "sdist" and leg.rpartition("-py")[0] not in TARGET_ARCHES:
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete")
runtime_architecture = None
if leg != "sdist":
runtime_name = f"{leg}.runtime.json"
Expand Down Expand Up @@ -391,17 +410,16 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]:
"native_properties": native_properties,
"fixture": fixture, "fixture_sha256": gate.fixture_digest(fixture),
"legs": [leg]}
if (len(seen_legs) != 13 or "sdist" not in seen_legs
or seen_variants != {f"universal2-apple-darwin-py{version}"
for version in ("3.12", "3.13", "3.14")}
or not rows):
raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete")
# Exact leg sets were required before I/O. Each of the twelve non-sdist
# primary legs requires a nonempty archive list and inserts or merges a row,
# so successful traversal cannot produce an empty runtime inventory.
return {"archives": sorted(rows.values(), key=lambda row: (row["key"], row["source_sha256"])),
"build_packages": sorted(build_rows.values(), key=lambda row: row["key"]),
"build_tools": sorted(tool_rows.values(), key=lambda row: row["key"])}


def main() -> None:
"""Validate captured runtime archive licenses and write the prescreen report."""
parser = argparse.ArgumentParser()
parser.add_argument("--verified-scope", required=True)
parser.add_argument("--scope-root", required=True)
Expand Down
2 changes: 2 additions & 0 deletions scripts/ci/release_dependency_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,7 @@ def _enumerate_cargo(capture: Path, *, source_root: Path | None = None,
raise GateError(CAPTURE_INCOMPLETE, "Cargo source checkout cannot be bound") from error

def source_blob(path: Path) -> bytes:
"""Read a regular Cargo declaration matching the selected Git commit."""
try:
relative = path.relative_to(bound_root)
if any((bound_root / parent).is_symlink() for parent in (relative, *relative.parents)):
Expand Down Expand Up @@ -2126,6 +2127,7 @@ def _source_license_notice(source: Path | None, source_sha: str, subject: str,
raise GateError(CAPTURE_INCOMPLETE, "source notice needs an exact release commit")

def blob(path: str) -> bytes:
"""Read a size-bounded, regular notice blob from the selected Git commit."""
entry = subprocess.check_output(
["git", "-C", str(source), "ls-tree", source_sha, "--", path], text=True)
if not entry.startswith("100644 blob "):
Expand Down
4 changes: 4 additions & 0 deletions scripts/ci/resolve_base_rust_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@


def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None:
"""Read a candidate toolchain declaration from the exact base commit."""
completed = subprocess.run(
["git", "-C", str(repo_root), "show", f"{base_sha}:{path}"],
check=False,
Expand All @@ -34,6 +35,7 @@ def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None:


def _channel(content: str) -> str | None:
"""Extract a TOML or legacy channel without accepting custom toolchains."""
try:
toolchain = tomllib.loads(content).get("toolchain")
except tomllib.TOMLDecodeError:
Expand All @@ -47,6 +49,7 @@ def _channel(content: str) -> str | None:


def resolve(repo_root: pathlib.Path, base_sha: str) -> str:
"""Select an exact base release or the documented central fallback."""
if not SHA_RE.fullmatch(base_sha):
raise ValueError("base SHA must be a full 40-character commit id")
for path in PIN_FILES:
Expand All @@ -66,6 +69,7 @@ def resolve(repo_root: pathlib.Path, base_sha: str) -> str:


def main(argv: list[str] | None = None) -> int:
"""Print the base-bound toolchain and fail on invalid source identity."""
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", required=True, type=pathlib.Path)
parser.add_argument("--base-sha", required=True)
Expand Down
Loading
Loading