diff --git a/.github/ISSUE_TEMPLATE/device-compatibility.yml b/.github/ISSUE_TEMPLATE/device-compatibility.yml index 473d475ad..6d3e4c037 100644 --- a/.github/ISSUE_TEMPLATE/device-compatibility.yml +++ b/.github/ISSUE_TEMPLATE/device-compatibility.yml @@ -14,6 +14,22 @@ body: placeholder: 1.6.18 validations: required: true + - type: input + id: tested-release-tag + attributes: + label: Tested release tag (or "unknown") + description: Use the tag of the binary actually tested, e.g. v1.6.40; do not substitute today's latest release. An unknown tag cannot establish a release-specific field-test claim. + placeholder: v1.6.40 or unknown + validations: + required: true + - type: input + id: tested-source-commit + attributes: + label: Tested release source commit (if documented) + description: Optional for initial submission. For release-specific publication, the maintainer must verify the exact source commit against the tagged release. + placeholder: 40-character commit SHA or unknown + validations: + required: false - type: input id: evidence-date attributes: diff --git a/docs/evidence-intake-review.md b/docs/evidence-intake-review.md index 663b01f4e..285596fd6 100644 --- a/docs/evidence-intake-review.md +++ b/docs/evidence-intake-review.md @@ -32,4 +32,14 @@ A maintainer may open a separate PR to update `landing/device-evidence.json` and 4. Reconcile registry, service-to-record links, coverage gaps, English/Indonesian pages and claim boundaries; run source, rendered-site, adoption/field-proof and exact-head PR CI. 5. Merge only after review and required checks pass; verify the final production Pages deployment. The published matrix is authoritative only after this gate. +## 4. Link a reviewed field test to an exact release + +The `releaseTraceability.reviewedTests` ledger inside `landing/device-evidence.json` starts empty. It is the only registry list of accepted **release-specific field-test records**; historical service statuses, an issue, a merge, package SHA-256 and green CI never populate it automatically. The current stable package identity comes only from `landing/latest.json`, not the profile date or a guessed tested version. + +For an accepted field test, add **one ledger record per profile, service and exact release** in a separate reviewed PR. The record requires: unique `id`, existing `profileId`, declared `service`, actual ISO `testDate`, exact `arsasVersion`, `releaseTag`, 40-character `sourceCommit`, exact tagged `releaseUrl`, bounded `result`, `evidenceKind` (`sanitized-field-test`), detailed `expectedObserved`, explicit `conditions`, `deviceDisclosure`, authorized sanitized `publicEvidenceUrl`, `reviewIssueUrl`, and `reviewPrUrl`. Link a public, safely redacted test record—not an implementation PR, private attachment or unsupported issue assertion. The maintainer confirms that the public material genuinely describes the tested service and release; CI can check internal consistency, **not the physical truth** of a relay test. + +For a record naming the currently published stable version, its tag and source commit must match `latest.json`. A historical tagged release must use its own exact version, tag, source commit and release URL, not a retroactive current-stable mapping. An older test never becomes a current-stable retest solely because the website or registry changed. Negative and conditional results remain bounded and must not be silently promoted to success. A sanitized diagnostic alone without a documented field test remains review material, not a release-specific field-test record. + +Update the English and Indonesian traceability surfaces, record count, release-specific rows and current-stable state in the **same PR**. Reconcile any profile `lastRetest` claim, service status and coverage plan only with the matching reviewed record. Source/rendered/adoption CI must reject ledger–page drift and mismatched current release identity. Never backfill the July 2026 profiles' unknown tested version merely from present-day release metadata. + An issue may remain open or be closed without a registry change. A green CI validates consistency of the published claims and links, **not the physical truth of a device test**. diff --git a/landing/adoption.css b/landing/adoption.css index 1027de769..6b29f24db 100644 --- a/landing/adoption.css +++ b/landing/adoption.css @@ -160,6 +160,7 @@ line-height: 1.55; } .matrix-boundary strong { color: var(--text); } +.evidence-gap-panel [data-release-source] code { overflow-wrap: anywhere; word-break: break-word; } .evidence-profile .evidence-trace { margin: .7rem 0 .9rem; padding: .7rem .8rem; font-size: .76rem; } .evidence-profile .evidence-records { margin: .65rem 0 .85rem; font-size: .78rem; line-height: 1.55; } .evidence-records a { color: #dff7ff; text-underline-offset: 2px; } diff --git a/landing/device-evidence.json b/landing/device-evidence.json index b75f531e1..053b20c79 100644 --- a/landing/device-evidence.json +++ b/landing/device-evidence.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "product": "ARSAS", - "updatedAt": "2026-09-22", + "updatedAt": "2026-09-23", "namedDeviceCount": 0, "statusVocabulary": { "verified": "Repeated evidence exists for the declared historical service scope. The tested ARSAS version must be disclosed when known; this is not IEC 61850 conformance certification.", @@ -141,5 +141,31 @@ "Positive and negative service result with sanitized diagnostics and acquisition conditions" ] } + }, + "releaseTraceability": { + "schemaVersion": 1, + "currentStableSource": "latest.json", + "reviewPolicyPath": "docs/evidence-intake-review.md", + "currentStableFieldTestState": "not-publicly-documented", + "reviewedTests": [], + "requiredRecordFields": [ + "id", + "profileId", + "service", + "testDate", + "arsasVersion", + "releaseTag", + "sourceCommit", + "releaseUrl", + "result", + "evidenceKind", + "expectedObserved", + "conditions", + "deviceDisclosure", + "publicEvidenceUrl", + "reviewIssueUrl", + "reviewPrUrl" + ], + "claimBoundary": "A release package identity, green CI, an issue, and implementation history are not evidence of a field test. Only a separately reviewed service-specific public test can enter reviewedTests; absence of entries is not a product failure." } } diff --git a/landing/templates/bukti-kompatibilitas.html b/landing/templates/bukti-kompatibilitas.html index 51fbddcd8..b6eb19024 100644 --- a/landing/templates/bukti-kompatibilitas.html +++ b/landing/templates/bukti-kompatibilitas.html @@ -62,6 +62,13 @@

Lihat service IEC 61850 mana yang memiliki evidence publik—dan mana yang b

Cara membacanya: satu service yang berhasil tidak berarti service lain otomatis bekerja, dan tidak ada row yang menjadi sertifikasi conformance IEC 61850. Buka profile di bawah untuk melihat kondisi exact dan engineering record publik di balik statusnya.

+
+
Evidence → release exact

Identitas release bukan bukti pengujian field.

Paket release bisa diperiksa; interoperabilitas memerlukan hasil pengujian terpisah untuk service tertentu.

+

Paket stable saat ini: v{{STABLE_VERSION}} · tag {{STABLE_TAG}} · source commit {{STABLE_SOURCE_COMMIT}}. Periksa release exact atau verifikasi SHA-256 dan provenance paket. Evidence paket/CI bukan bukti pengujian device.

+

Profile A · Juli 2026: riwayat file-service, versi ARSAS saat diuji belum tercatat secara publik; kondisi dan jejak engineering. Tidak otomatis dikaitkan dengan stable saat ini.

+

Profile B · Juli 2026: riwayat RCB/SCL, versi ARSAS saat diuji belum tercatat secara publik; kondisi dan jejak engineering. Tidak otomatis dikaitkan dengan stable saat ini.

+

Record pengujian field per-release yang diterima dan dapat ditelusuri secara publik: 0. Retest pada v{{STABLE_VERSION}} belum terdokumentasi secara publik dalam registry. Ini menunjukkan cakupan publikasi, bukan kegagalan device atau software. Record baru wajib memuat versi, tag, source commit, service, tanggal aktual, hasil, kondisi, evidence publik yang disanitasi, dan PR review. Periksa ledger pengujian release dan persyaratan review.

+
Gap evidence · capture berikutnya

Apa yang belum dibuktikan secara publik pada kedua profil ini?

GOOSE dan Control berstatus Not tested pada kedua profil. Ini gap evidence field publik, bukan pernyataan bahwa ARSAS tidak memiliki kemampuan tersebut. Service yang tidak dicantumkan oleh satu profil tetap Not declared, bukan Not tested.

diff --git a/landing/templates/compatibility.html b/landing/templates/compatibility.html index 8bc5e8494..1b65ebeb2 100644 --- a/landing/templates/compatibility.html +++ b/landing/templates/compatibility.html @@ -62,6 +62,13 @@

See exactly which IEC 61850 services have public evidence—and which do not

How to read this: one successful service does not imply another service works, and no row certifies IEC 61850 conformance. Open the profile below to see the exact conditions and public engineering records behind each status.

+
+
Evidence → exact release

Release identity and field-test evidence are different records.

The release is verifiable as a package; interoperability requires a separate service-specific field result.

+

Current stable package: v{{STABLE_VERSION}} · tag {{STABLE_TAG}} · source commit {{STABLE_SOURCE_COMMIT}}. Inspect the exact release or verify SHA-256 and package provenance. Package/CI evidence does not prove a device test.

+

Profile A · July 2026: file-service history, tested ARSAS version not publicly recorded; conditions and engineering trail. Not associated with the current stable by inference.

+

Profile B · July 2026: RCB/SCL history, tested ARSAS version not publicly recorded; conditions and engineering trail. Not associated with the current stable by inference.

+

Accepted, publicly linked release-specific field-test records: 0. No retest on v{{STABLE_VERSION}} is publicly documented in the registry. This describes publication coverage, not a device or software failure. Future reviewed records must identify the exact version, tag, source commit, service, actual date, outcome, conditions, sanitized public evidence and review PR. Inspect the release-test ledger and review requirements.

+
Evidence gaps · next capture

What is missing from these two published profiles?

GOOSE and Control are marked Not tested in both profiles. This is a gap in public field evidence, not a claim that ARSAS lacks either capability. A service omitted from one profile remains Not declared, not Not tested.

diff --git a/landing/templates/technical-review.html b/landing/templates/technical-review.html index d1c0e1839..68164eec4 100644 --- a/landing/templates/technical-review.html +++ b/landing/templates/technical-review.html @@ -34,7 +34,7 @@ {{> trust-architecture}} -
Current evidence

Claims must point to inspectable behavior.

  • Current application screenshots and visible UI states
  • Deterministic source and rendered-site validation
  • Tagged release package identity, SHA-256, SPDX SBOM and provenance evidence
  • Exact source commit, source history, CI regression evidence and implementation boundaries
  • Published July 2026 field profiles are historical; their capture-version is not publicly recorded and a retest on v{{STABLE_VERSION}} is not documented. Review service-level scope.
Not a conformance certificate

Evidence narrows causes; it does not replace project acceptance.

  • Final conclusions depend on the approved design and test procedure
  • Vendor-specific behavior must be checked against vendor documentation
  • Network access, permissions and IED configuration remain project inputs
  • Independent verification remains required where the project demands it
+
Current evidence

Claims must point to inspectable behavior.

  • Current application screenshots and visible UI states
  • Deterministic source and rendered-site validation
  • Tagged release package identity, SHA-256, SPDX SBOM and provenance evidence
  • Exact source commit, source history, CI regression evidence and implementation boundaries
  • Published July 2026 field profiles are historical; their capture-version is not publicly recorded and a retest on v{{STABLE_VERSION}} is not documented. Review service-level scope and trace evidence to an exact release.
Not a conformance certificate

Evidence narrows causes; it does not replace project acceptance.

  • Final conclusions depend on the approved design and test procedure
  • Vendor-specific behavior must be checked against vendor documentation
  • Network access, permissions and IED configuration remain project inputs
  • Independent verification remains required where the project demands it
Claim governance

Four labels keep product communication honest.

ARSAS pages and guides are written to preserve the difference between implemented behavior and future intent.

Available

Implemented and inspectable

The workflow is present in the current software and can be examined through the application, release artifact or source.

Conditional

Depends on device or project conditions

Support can depend on IED services, writable attributes, network capture access, control model, permissions or configured engineering data.

Preview

Useful but still maturing

The workflow is available for evaluation while interoperability breadth, UX or diagnostic coverage continues to mature.

Roadmap

Planned, not promised as current

Roadmap content describes direction and is never presented as part of the current stable capability set.

diff --git a/scripts/validate-adoption-proof.py b/scripts/validate-adoption-proof.py index 29642cfcb..5c87a63e8 100644 --- a/scripts/validate-adoption-proof.py +++ b/scripts/validate-adoption-proof.py @@ -7,6 +7,7 @@ import json import re import sys +from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -284,24 +285,105 @@ def main() -> int: errors.append("device-evidence.json: each uncovered service needs a concrete capture request") next_evidence = {} stable_retest = plan.get("stableRetest") - if not isinstance(stable_retest, dict) or stable_retest.get("state") != "not-publicly-documented" or not isinstance(stable_retest.get("requirements"), list) or len(stable_retest["requirements"]) != 3 or any(not isinstance(item, str) or len(item.strip()) < 20 for item in stable_retest["requirements"]): + if not isinstance(stable_retest, dict) or stable_retest.get("state") not in {"not-publicly-documented", "documented-with-reviewed-records"} or not isinstance(stable_retest.get("requirements"), list) or len(stable_retest["requirements"]) != 3 or any(not isinstance(item, str) or len(item.strip()) < 20 for item in stable_retest["requirements"]): errors.append("device-evidence.json: incomplete current-stable retest intake requirements") - if any(isinstance(p, dict) and p.get("lastRetest") is not None for p in profiles) and isinstance(stable_retest, dict) and stable_retest.get("state") == "not-publicly-documented": - errors.append("device-evidence.json: retest declaration changed; refresh coverage plan and page claims") + + # R6.5: a reviewed field-test ledger is distinct from package identity and + # historical engineering trails. There are deliberately no accepted records yet. + trace = evidence.get("releaseTraceability") + required_fields = { + "id", "profileId", "service", "testDate", "arsasVersion", "releaseTag", + "sourceCommit", "releaseUrl", "result", "evidenceKind", + "expectedObserved", "conditions", "deviceDisclosure", + "publicEvidenceUrl", "reviewIssueUrl", "reviewPrUrl", + } + if not isinstance(trace, dict) or trace.get("schemaVersion") != 1 or trace.get("currentStableSource") != "latest.json" or trace.get("reviewPolicyPath") != "docs/evidence-intake-review.md": + errors.append("releaseTraceability: missing exact-release review contract") + trace = {} + if not isinstance(trace.get("requiredRecordFields"), list) or len(trace["requiredRecordFields"]) != len(required_fields) or set(trace["requiredRecordFields"]) != required_fields: + errors.append("releaseTraceability: reviewed record requirements drifted") + if not isinstance(trace.get("claimBoundary"), str) or len(trace["claimBoundary"]) < 100: + errors.append("releaseTraceability: missing package versus field-test boundary") + reviewed = trace.get("reviewedTests") + if not isinstance(reviewed, list): + errors.append("releaseTraceability: reviewedTests must be an explicit list") + reviewed = [] + exact_release = json.loads(read(LANDING / "latest.json", errors) or "{}") + current_version = exact_release.get("version") + current_tag = exact_release.get("tag") + current_commit = exact_release.get("sourceCommit") + record_ids: set[str] = set() + current_records: list[dict] = [] + for record in reviewed: + if not isinstance(record, dict) or set(record) != required_fields: + errors.append("releaseTraceability: record missing required provenance fields") + continue + rid = str(record["id"]) + if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)+", rid) or rid in record_ids: + errors.append(f"releaseTraceability: invalid or duplicate test ID {rid}") + record_ids.add(rid) + profile = next((p for p in profiles if isinstance(p, dict) and p.get("id") == record["profileId"]), None) + if profile is None or record["service"] not in profile.get("services", {}): + errors.append(f"{rid}: unknown profile or undeclared service") + elif profile["services"][record["service"]] != record["result"]: + errors.append(f"{rid}: reviewed result differs from the profile service status") + if record["result"] not in STATUSES - {"not-tested"}: + errors.append(f"{rid}: not-tested cannot be a completed field result") + if record["evidenceKind"] != "sanitized-field-test": + errors.append(f"{rid}: implementation history or diagnostic alone is not a field test") + version = str(record["arsasVersion"]) + tag = str(record["releaseTag"]) + if not re.fullmatch(r"\d+\.\d+\.\d+", version) or tag != "v" + version: + errors.append(f"{rid}: invalid exact version/tag association") + if not re.fullmatch(r"[0-9a-f]{40}", str(record["sourceCommit"])): + errors.append(f"{rid}: invalid immutable source commit") + if record["releaseUrl"] != f"https://github.com/masarray/arsas/releases/tag/{tag}": + errors.append(f"{rid}: release URL does not match tested tag") + try: + tested_at = date.fromisoformat(str(record["testDate"])) + if tested_at > date.today(): + errors.append(f"{rid}: test date cannot be in the future") + except ValueError: + errors.append(f"{rid}: invalid actual test date") + if len(str(record["expectedObserved"]).strip()) < 30 or not isinstance(record["conditions"], list) or len(record["conditions"]) < 2 or any(len(str(v).strip()) < 10 for v in record["conditions"]) or len(str(record["deviceDisclosure"]).strip()) < 12: + errors.append(f"{rid}: incomplete sanitized test context") + for key, suffix in (("publicEvidenceUrl", "issues"), ("reviewIssueUrl", "issues"), ("reviewPrUrl", "pull")): + if not re.fullmatch(rf"https://github\.com/masarray/arsas/{suffix}/[1-9]\d*", str(record[key])): + errors.append(f"{rid}: {key} must be a public ARSAS {suffix} record") + if version == current_version: + current_records.append(record) + if tag != current_tag or record["sourceCommit"] != current_commit or record["releaseUrl"] != exact_release.get("releaseUrl"): + errors.append(f"{rid}: claimed current stable test does not match latest.json") + if isinstance(record["testDate"], str) and record["testDate"] < str(exact_release.get("publishedAtUtc", ""))[:10]: + errors.append(f"{rid}: current release test predates publication of its exact tag") + if profile is not None and profile.get("lastRetest") is not None: + last = profile["lastRetest"] + if isinstance(last, dict) and last.get("date") == record["testDate"] and last.get("arsasVersion") == version and record["publicEvidenceUrl"] not in last.get("evidenceLinks", []): + errors.append(f"{rid}: profile retest is not linked to its public field record") + expected_state = "documented-with-reviewed-records" if current_records else "not-publicly-documented" + if trace.get("currentStableFieldTestState") != expected_state: + errors.append("releaseTraceability: current stable field-test state disagrees with accepted records") + if isinstance(stable_retest, dict) and stable_retest.get("state") != expected_state: + errors.append("coveragePlan: current stable retest state disagrees with accepted records") + for profile in profiles: + if isinstance(profile, dict) and isinstance(profile.get("lastRetest"), dict): + last = profile["lastRetest"] + if not any(record.get("profileId") == profile["id"] and record.get("testDate") == last.get("date") and record.get("arsasVersion") == last.get("arsasVersion") and record.get("publicEvidenceUrl") in last.get("evidenceLinks", []) for record in reviewed if isinstance(record, dict)): + errors.append(f"{profile['id']}: profile retest lacks a matching reviewed release record") issue_form = read(ROOT / ".github" / "ISSUE_TEMPLATE" / "device-compatibility.yml", errors) require_values(issue_form, "device-compatibility.yml", ("id: evidence-date", "id: evidence-kind", "id: prior-profile", "actual test date", "engineering or implementation history only"), errors, "R6.3 evidence intake") review_url = "https://github.com/masarray/arsas/blob/main/docs/evidence-intake-review.md" require_values(issue_form, "device-compatibility.yml", ( review_url, "one issue = one bounded service result", "id: expected-observed", - "id: public-evidence-link", "a submission, not verified evidence", + "id: public-evidence-link", "id: tested-release-tag", "id: tested-source-commit", "a submission, not verified evidence", "separate registry PR", "raw private captures", ), errors, "R6.4 evidence intake/review gate") review_policy = read(ROOT / "docs" / "evidence-intake-review.md", errors) require_values(review_policy, "docs/evidence-intake-review.md", ( "submitted, not verified", "Privacy first", "Maintainer review gate", "Promote only with a reviewed PR", "actual test date", "exact tested ARSAS version", - "not a retest", "registry", "not the physical truth", + "not a retest", "registry", "not the physical truth", "releaseTraceability.reviewedTests", "latest.json", ), errors, "R6.4 review policy") for name in ("compatibility.html", "bukti-kompatibilitas.html"): @@ -316,6 +398,32 @@ def main() -> int: require_values(text, name, proof_route, errors, "R6 evaluator proof route") if 'data-evidence-intake="submitted-not-verified"' not in text or review_url not in text: errors.append(f"{name}: missing R6.4 submission-to-review policy route") + if 'data-release-traceability="reviewed-tests-only"' not in text or 'data-release-source="latest.json"' not in text: + errors.append(f"{name}: missing release-to-test traceability contract") + if f'data-reviewed-release-test-count="{len(reviewed)}"' not in text or f'data-current-stable-field-test="{expected_state}"' not in text: + errors.append(f"{name}: accepted test count/current release state disagrees with registry") + for token in ("{{STABLE_VERSION}}", "{{STABLE_TAG}}", "{{STABLE_SOURCE_COMMIT}}", "{{RELEASE_URL}}"): + if token not in text: + errors.append(f"{name}: missing exact release template token {token}") + for profile_id in profile_ids: + if f'data-release-trace-profile="{profile_id}"' not in text: + errors.append(f"{name}: missing historical release boundary {profile_id}") + displayed = re.findall(r'data-release-test-record="([^"]+)"', text) + if len(displayed) != len(set(displayed)) or set(displayed) != record_ids: + errors.append(f"{name}: release-test rows differ from reviewed ledger") + if not reviewed and 'data-release-records="none"' not in text: + errors.append(f"{name}: unverified empty ledger is not explicit") + if reviewed and 'data-release-records="none"' in text: + errors.append(f"{name}: stale empty-ledger claim remains after test promotion") + for record in reviewed: + if not isinstance(record, dict) or "id" not in record: + continue + start = text.find(f'data-release-test-record="{record["id"]}"') + end = text.find("", start) if start >= 0 else -1 + row = text[start:end] if end >= 0 else "" + for value in (record["profileId"], record["service"], record["testDate"], record["arsasVersion"], record["releaseTag"], record["sourceCommit"], record["publicEvidenceUrl"], record["reviewPrUrl"]): + if str(value) not in row: + errors.append(f"{name}: incomplete reviewed test row {record['id']}") if name == "compatibility.html": require_values(text, name, ( "Submit → maintainer review → registry PR", "a GitHub issue is a report", diff --git a/scripts/validate-product-build.py b/scripts/validate-product-build.py index 0de15a3e0..027370aeb 100644 --- a/scripts/validate-product-build.py +++ b/scripts/validate-product-build.py @@ -242,7 +242,7 @@ def main() -> int: for stale in ("Have the software?", "Connect an approved IED", "Follow the first connection"): if stale in home_text: errors.append(f"{home}: stale English homepage localization remains: {stale}") technical_review_text = (site / "technical-review.html").read_text(encoding="utf-8") if (site / "technical-review.html").is_file() else "" - for value in ('data-trust-architecture="true"', "SPDX SBOM", "CI regression evidence", stable_source, "Not a conformance certificate", "Review field interoperability evidence", "Verify the stable release", "July 2026 field profiles", f"v{latest.get('version')}"): + for value in ('data-trust-architecture="true"', "SPDX SBOM", "CI regression evidence", stable_source, "Not a conformance certificate", "Review field interoperability evidence", "Verify the stable release", "July 2026 field profiles", "compatibility.html#release-traceability", f"v{latest.get('version')}"): if value not in technical_review_text: errors.append(f"technical-review.html: missing technical-review proof/freshness value {value}") for page in ("download.html", "unduh.html", "release-notes.html", "catatan-rilis.html"): release_text = (site / page).read_text(encoding="utf-8") if (site / page).is_file() else "" @@ -312,6 +312,20 @@ def main() -> int: field_evidence = {} if field_evidence.get("schemaVersion") != 2: errors.append("rendered field evidence must use freshness schema v2") + release_trace = field_evidence.get("releaseTraceability", {}) + reviewed_release_tests = release_trace.get("reviewedTests", []) if isinstance(release_trace, dict) else [] + if not isinstance(reviewed_release_tests, list): + errors.append("rendered release traceability ledger must be a list") + reviewed_release_tests = [] + current_reviewed = [ + record for record in reviewed_release_tests if isinstance(record, dict) + and record.get("arsasVersion") == latest.get("version") + and record.get("releaseTag") == latest.get("tag") + and record.get("sourceCommit") == latest.get("sourceCommit") + ] + release_test_state = "documented-with-reviewed-records" if current_reviewed else "not-publicly-documented" + if not isinstance(release_trace, dict) or release_trace.get("schemaVersion") != 1 or release_trace.get("currentStableSource") != "latest.json" or release_trace.get("currentStableFieldTestState") != release_test_state: + errors.append("rendered registry exact-release evidence state differs from current release identity") for page in ("compatibility.html", "bukti-kompatibilitas.html"): matrix_text = (site / page).read_text(encoding="utf-8") if (site / page).is_file() else "" contract = ( @@ -323,6 +337,40 @@ def main() -> int: if value not in matrix_text: errors.append(f"{page}: missing rendered interoperability proof value {value}") for value in ('data-evidence-intake="submitted-not-verified"', 'docs/evidence-intake-review.md'): if value not in matrix_text: errors.append(f"{page}: missing rendered R6.4 review gate {value}") + release_contract = ( + 'data-release-traceability="reviewed-tests-only"', + 'data-release-source="latest.json"', + f'data-release-version="{latest.get("version")}"', + f'data-release-tag="{latest.get("tag")}"', + f'data-release-commit="{latest.get("sourceCommit")}"', + f'data-current-stable-field-test="{release_test_state}"', + f'data-reviewed-release-test-count="{len(reviewed_release_tests)}"', + str(latest.get("releaseUrl", "")), + ) + for value in release_contract: + if value not in matrix_text: errors.append(f"{page}: missing rendered R6.5 release trace {value}") + for token in ("{{STABLE_TAG}}", "{{STABLE_SOURCE_COMMIT}}", "{{RELEASE_URL}}"): + if token in matrix_text: errors.append(f"{page}: unrendered exact-release identity token {token}") + for profile in field_evidence.get("profiles", []): + if isinstance(profile, dict) and f'data-release-trace-profile="{profile.get("id")}"' not in matrix_text: + errors.append(f"{page}: missing historical release boundary for {profile.get('id')}") + shown = re.findall(r'data-release-test-record="([^"]+)"', matrix_text) + expected = [str(record.get("id")) for record in reviewed_release_tests if isinstance(record, dict)] + if len(shown) != len(set(shown)) or sorted(shown) != sorted(expected): + errors.append(f"{page}: rendered reviewed field-test rows differ from ledger") + if not reviewed_release_tests and 'data-release-records="none"' not in matrix_text: + errors.append(f"{page}: empty reviewed field-test ledger is not disclosed") + if reviewed_release_tests and 'data-release-records="none"' in matrix_text: + errors.append(f"{page}: stale zero-test claim after promotion") + for record in reviewed_release_tests: + if not isinstance(record, dict) or "id" not in record: + continue + start = matrix_text.find(f'data-release-test-record="{record["id"]}"') + end = matrix_text.find("", start) if start >= 0 else -1 + row = matrix_text[start:end] if end >= 0 else "" + for key in ("profileId", "service", "testDate", "arsasVersion", "releaseTag", "sourceCommit", "publicEvidenceUrl", "reviewPrUrl"): + if str(record.get(key)) not in row: + errors.append(f"{page}: missing public release-test field {record['id']}/{key}") for value in ('data-evidence-freshness="true"', f"v{latest.get('version')}", 'data-tested-version="not-recorded"', 'data-current-stable-retest="not-documented"'): if value not in matrix_text: errors.append(f"{page}: missing rendered historical/current stable distinction {value}") if "{{STABLE_VERSION}}" in matrix_text: @@ -340,8 +388,16 @@ def main() -> int: for url in urls: if f'href="{url}"' not in record_block: errors.append(f"{page}: missing {profile_id}/{service} public engineering trail") - if profile.get("testedArsasVersion") is not None or profile.get("lastRetest") is not None: - errors.append(f"{page}: registry version/retest changed; update the displayed provenance before publishing") + if profile.get("testedArsasVersion") is not None: + errors.append(f"{page}: historical capture version cannot be inferred from a later release") + if isinstance(profile.get("lastRetest"), dict) and not any( + isinstance(record, dict) and record.get("profileId") == profile_id + and record.get("testDate") == profile["lastRetest"].get("date") + and record.get("arsasVersion") == profile["lastRetest"].get("arsasVersion") + and record.get("publicEvidenceUrl") in profile["lastRetest"].get("evidenceLinks", []) + for record in reviewed_release_tests + ): + errors.append(f"{page}: profile retest lacks matching accepted release evidence") if not GUIDES.issubset(set(expected_pages)): errors.append("troubleshooting guides are missing from the build") sitemap = site / "sitemap.xml" diff --git a/scripts/validate-product-source.py b/scripts/validate-product-source.py index 20f4f91f3..2edcd0fd0 100644 --- a/scripts/validate-product-source.py +++ b/scripts/validate-product-source.py @@ -293,7 +293,7 @@ def main() -> int: if stale in rendered: errors.append(f"{label}: stale English homepage localization remains: {stale}") if path == "technical-review.html": - for value in ('{{> trust-architecture}}', "SPDX SBOM", "CI regression evidence", "Not a conformance certificate", "ARSAS Open-Source Reliability & Technical Review", "Review field interoperability evidence", "Verify the stable release", "July 2026 field profiles", "{{STABLE_VERSION}}"): + for value in ('{{> trust-architecture}}', "SPDX SBOM", "CI regression evidence", "Not a conformance certificate", "ARSAS Open-Source Reliability & Technical Review", "Review field interoperability evidence", "Verify the stable release", "July 2026 field profiles", "{{STABLE_VERSION}}", "compatibility.html#release-traceability"): if value not in raw and value not in rendered: errors.append(f"{label}: missing technical-review proof route value {value}") if path in {"compatibility.html", "bukti-kompatibilitas.html"}: @@ -308,6 +308,19 @@ def main() -> int: for value in ('data-evidence-intake="submitted-not-verified"', 'docs/evidence-intake-review.md'): if value not in raw: errors.append(f"{label}: missing R6.4 intake/review route {value}") + for value in ( + 'data-release-traceability="reviewed-tests-only"', + 'data-release-source="latest.json"', + 'data-reviewed-release-test-count="0"', + 'data-current-stable-field-test="not-publicly-documented"', + 'data-release-records="none"', + '{{STABLE_TAG}}', '{{STABLE_SOURCE_COMMIT}}', '{{RELEASE_URL}}', + ): + if value not in raw: + errors.append(f"{label}: missing R6.5 exact-release versus field-test distinction {value}") + for profile_id in ("field-profile-a-file-service", "field-profile-b-rcb-export"): + if f'data-release-trace-profile="{profile_id}"' not in raw: + errors.append(f"{label}: missing historical release trace {profile_id}") for value in ('data-evidence-freshness="true"', '{{STABLE_VERSION}}', 'data-tested-version="not-recorded"', 'data-current-stable-retest="not-documented"'): if value not in raw: errors.append(f"{label}: missing historical field/retest disclosure {value}")