Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions landing/templates/analyzer-goose-iec61850.html

Large diffs are not rendered by default.

41 changes: 29 additions & 12 deletions landing/templates/goose-analyzer.html

Large diffs are not rendered by default.

45 changes: 31 additions & 14 deletions landing/templates/scl-workspace.html

Large diffs are not rendered by default.

45 changes: 31 additions & 14 deletions landing/templates/workspace-scl-iec61850.html

Large diffs are not rendered by default.

137 changes: 86 additions & 51 deletions scripts/validate-adoption-proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"connect-ied-ip-arsas.html": "cara-hubungkan-ied-ip-arsas.html",
"mms-client.html": "mms-client-iec61850.html",
"smart-reporting.html": "smart-reporting-iec61850.html",
"goose-analyzer.html": "analyzer-goose-iec61850.html",
"scl-workspace.html": "workspace-scl-iec61850.html",
"io-list-fat-evidence.html": "bukti-fat-iolist-iec61850.html",
"faq.html": "faq-arsas.html",
"compatibility.html": "bukti-kompatibilitas.html",
Expand All @@ -38,6 +40,13 @@ def read(path: Path, errors: list[str]) -> str:
return ""


def require_values(text: str, label: str, values: tuple[str, ...], errors: list[str], contract: str) -> None:
lower = text.lower()
for value in values:
if value.lower() not in lower:
Comment on lines +44 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve case-sensitive contract checks

When this helper validates structural tokens, lowercasing both operands allows invalid downstream syntax to pass—for example, packageVerificationCode can become packageverificationcode, or a GitHub issue-form body: key can become Body:. The calls at lines 243 and 256 previously used exact matching because YAML/JSON keys and SPDX values are case-sensitive, so retain exact matching for those contracts or add an explicit case-sensitive mode while folding only prose checks.

Useful? React with 👍 / 👎.

errors.append(f"{label}: missing {contract} {value}")


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--site", default="")
Expand All @@ -59,16 +68,23 @@ def main() -> int:
if not isinstance(entry, dict) or entry.get("language") != language or entry.get("alternates") != expected:
errors.append(f"invalid adoption localization contract for {path}")

quick = read(TEMPLATES / "quick-start.html", errors)
quick_id = read(TEMPLATES / "panduan-mulai-arsas.html", errors)
for text, label in ((quick, "quick-start.html"), (quick_id, "panduan-mulai-arsas.html")):
for value in ("quick-step-number", "TCP port 102", "Npcap", "MMS", "live", "quality", "timestamp", "reporting", "polling", "diagnostic", "control"):
if value.lower() not in text.lower():
errors.append(f"{label}: missing onboarding contract {value}")
quick_pairs = (
("quick-start.html", "io-list-fat-evidence.html"),
("panduan-mulai-arsas.html", "bukti-fat-iolist-iec61850.html"),
)
for name, continuation in quick_pairs:
text = read(TEMPLATES / name, errors)
require_values(
text,
name,
("quick-step-number", "TCP port 102", "Npcap", "MMS", "live", "quality", "timestamp", "reporting", "polling", "diagnostic", "control"),
errors,
"onboarding contract",
)
if text.count('class="quick-step"') != 7:
errors.append(f"{label}: expected seven quick-start steps")
if "io-list-fat-evidence.html" not in text and "bukti-fat-iolist-iec61850.html" not in text:
errors.append(f"{label}: missing IO List FAT continuation path")
errors.append(f"{name}: expected seven quick-start steps")
if continuation not in text:
errors.append(f"{name}: missing IO List FAT continuation path")

learning_contracts = {
"learning-center.html": ("Learn IEC 61850", "What is IEC 61850", "Connect your first IED", "troubleshooting", "CollectionPage"),
Expand All @@ -78,12 +94,11 @@ def main() -> int:
"connect-ied-ip-arsas.html": ("TCP port 102", "Add IED", "MMS association", "quality", "timestamp", "HowTo"),
"cara-hubungkan-ied-ip-arsas.html": ("TCP port 102", "Add IED", "association MMS", "quality", "timestamp", "HowTo"),
}
learning_hubs = {"learning-center.html", "pusat-belajar-iec61850.html"}
for name, required_values in learning_contracts.items():
text = read(TEMPLATES / name, errors)
for value in required_values:
if value.lower() not in text.lower():
errors.append(f"{name}: missing learning contract {value}")
if "learning-center.html" not in text and "pusat-belajar-iec61850.html" not in text and name not in {"learning-center.html", "pusat-belajar-iec61850.html"}:
require_values(text, name, required_values, errors, "learning contract")
if name not in learning_hubs and "learning-center.html" not in text and "pusat-belajar-iec61850.html" not in text:
errors.append(f"{name}: missing Learning Center continuation path")

capability_tutorials = {
Expand All @@ -103,39 +118,54 @@ def main() -> int:
"Takeaway 30 detik", "DataSet", "BRCB", "URCB", "General Interrogation",
"Success criteria", "Bila Reporting silent", "HowTo", "FAQPage", "pusat-belajar-iec61850.html",
),
"goose-analyzer.html": (
"30-second takeaway", "stNum", "sqNum", "Time Allowed to Live",
"Success criteria", "If the result is wrong", "HowTo", "FAQPage", "learning-center.html",
),
"analyzer-goose-iec61850.html": (
"Takeaway 30 detik", "stNum", "sqNum", "Time Allowed to Live",
"Success criteria", "Bila hasil salah", "HowTo", "FAQPage", "pusat-belajar-iec61850.html",
),
"scl-workspace.html": (
"30-second takeaway", "ICD", "CID", "SCD", "IID",
"Success criteria", "If the file is rejected", "HowTo", "FAQPage", "learning-center.html",
),
"workspace-scl-iec61850.html": (
"Takeaway 30 detik", "ICD", "CID", "SCD", "IID",
"Success criteria", "Bila file ditolak", "HowTo", "FAQPage", "pusat-belajar-iec61850.html",
),
}
for name, required_values in capability_tutorials.items():
text = read(TEMPLATES / name, errors)
for value in required_values:
if value.lower() not in text.lower():
errors.append(f"{name}: missing capability tutorial contract {value}")
require_values(text, name, required_values, errors, "capability tutorial contract")
if text.count('<details class="faq-item">') != 3:
errors.append(f"{name}: expected three visible capability FAQ items")
expected_step_class = 'class="reporting-step"' if name.startswith("smart-reporting") else 'class="step"'
if text.count(expected_step_class) != 6:
step_class = 'class="reporting-step"' if name.startswith("smart-reporting") else 'class="step"'
if text.count(step_class) != 6:
errors.append(f"{name}: expected six capability tutorial steps")

for name in ("io-list-fat-evidence.html", "bukti-fat-iolist-iec61850.html"):
text = read(TEMPLATES / name, errors)
require_values(
text,
name,
("TestPointId", "OFF → ON", "ON → OFF", ".arsas", "Excel", "PDF", "quality", "timestamp", "acquisition source"),
errors,
"IO FAT contract",
)
lower = text.lower()
for value in ("TestPointId", "OFF → ON", "ON → OFF", ".arsas", "Excel", "PDF", "quality", "timestamp", "acquisition source"):
if value.lower() not in lower:
errors.append(f"{name}: missing IO FAT contract {value}")
if "mainline" not in lower or "stable" not in lower:
errors.append(f"{name}: missing mainline versus stable release boundary")
if "browser" not in lower or "third-party" not in lower:
errors.append(f"{name}: missing native PDF dependency boundary")

faq = read(TEMPLATES / "faq.html", errors)
faq_id = read(TEMPLATES / "faq-arsas.html", errors)
for text, label in ((faq, "faq.html"), (faq_id, "faq-arsas.html")):
for name in ("faq.html", "faq-arsas.html"):
text = read(TEMPLATES / name, errors)
if '"@type":"FAQPage"' not in text.replace(" ", ""):
errors.append(f"{label}: missing visible FAQPage structured data")
errors.append(f"{name}: missing visible FAQPage structured data")
if text.count('<details class="faq-item">') != 12:
errors.append(f"{label}: expected twelve visible FAQ items")
for value in ("SmartScreen", "Npcap", "SBO", "analytics", "Portable"):
if value.lower() not in text.lower():
errors.append(f"{label}: missing FAQ topic {value}")
errors.append(f"{name}: expected twelve visible FAQ items")
require_values(text, name, ("SmartScreen", "Npcap", "SBO", "analytics", "Portable"), errors, "FAQ topic")

evidence_path = LANDING / "device-evidence.json"
try:
Expand Down Expand Up @@ -168,24 +198,27 @@ def main() -> int:
if not isinstance(profile.get("conditions"), list) or len(profile["conditions"]) < 3:
errors.append(f"{profile_id}: evidence conditions are incomplete")

compatibility = read(TEMPLATES / "compatibility.html", errors)
compatibility_id = read(TEMPLATES / "bukti-kompatibilitas.html", errors)
for text, label in ((compatibility, "compatibility.html"), (compatibility_id, "bukti-kompatibilitas.html")):
for name in ("compatibility.html", "bukti-kompatibilitas.html"):
text = read(TEMPLATES / name, errors)
for profile_id in profile_ids:
if f'data-evidence-profile="{profile_id}"' not in text:
errors.append(f"{label}: missing evidence profile {profile_id}")
errors.append(f"{name}: missing evidence profile {profile_id}")
for status in ("verified", "conditional", "observed"):
if f'data-status="{status}"' not in text:
errors.append(f"{label}: missing status {status}")
errors.append(f"{name}: missing status {status}")
lower = text.lower()
if "device-evidence.json" not in text or not any(term in lower for term in ("conformance", "conformity")):
errors.append(f"{label}: missing machine-readable or conformance boundary")
errors.append(f"{name}: missing machine-readable or conformance boundary")

for name in ("guides.html", "panduan.html"):
text = read(TEMPLATES / name, errors)
for value in ("data-guide-filter", "data-guide-search", "data-guide-category", "data-guide-card", "guide-filter.js"):
if value not in text:
errors.append(f"{name}: missing guide-filter contract {value}")
require_values(
text,
name,
("data-guide-filter", "data-guide-search", "data-guide-category", "data-guide-card", "guide-filter.js"),
errors,
"guide-filter contract",
)
if text.count("data-guide-card") != 11:
errors.append(f"{name}: expected eleven filterable guide cards")
if "io-list-fat-evidence.html" not in text and "bukti-fat-iolist-iec61850.html" not in text:
Expand All @@ -207,22 +240,20 @@ def main() -> int:
errors.append("missing structured issue forms: " + ", ".join(sorted(missing_forms)))
for name in ISSUE_FORMS - {"config.yml"}:
text = read(issue_dir / name, errors)
for value in ("name:", "description:", "body:", "validations:"):
if value not in text:
errors.append(f"{name}: incomplete issue-form contract {value}")
require_values(text, name, ("name:", "description:", "body:", "validations:"), errors, "issue-form contract")
combined_forms = "\n".join(read(issue_dir / name, errors) for name in ISSUE_FORMS)
for value in ("ARSAS version", "Windows", "sanitized", "credentials", "confidential"):
if value.lower() not in combined_forms.lower():
errors.append(f"issue forms missing intake boundary {value}")
require_values(combined_forms, "issue forms", ("ARSAS version", "Windows", "sanitized", "credentials", "confidential"), errors, "intake boundary")

supply_workflow = read(ROOT / ".github" / "workflows" / "release-supply-chain.yml", errors)
for value in ("actions/attest@v4", "attestations: write", "id-token: write", "sha256sum --check", "generate-release-sbom.py", "ARSAS-Windows-x64-SBOM.spdx.json"):
if value not in supply_workflow:
errors.append(f"release supply-chain workflow missing contract {value}")
require_values(
supply_workflow,
"release-supply-chain.yml",
("actions/attest@v4", "attestations: write", "id-token: write", "sha256sum --check", "generate-release-sbom.py", "ARSAS-Windows-x64-SBOM.spdx.json"),
errors,
"supply-chain workflow contract",
)
sbom_generator = read(ROOT / "scripts" / "generate-release-sbom.py", errors)
for value in ("SPDX-2.3", "packageVerificationCode", "SHA256", "GPL-3.0-or-later"):
if value not in sbom_generator:
errors.append(f"SBOM generator missing contract {value}")
require_values(sbom_generator, "generate-release-sbom.py", ("SPDX-2.3", "packageVerificationCode", "SHA256", "GPL-3.0-or-later"), errors, "SBOM contract")

for name in ("download.html", "unduh.html", "release-notes.html", "catatan-rilis.html"):
text = read(TEMPLATES / name, errors)
Expand All @@ -247,7 +278,11 @@ def main() -> int:
for error in errors:
print(f"- {error}")
return 1
print("ARSAS adoption and field-proof validation passed: 62 pages, 21 localized pages, beginner learning, bilingual MMS and Reporting tutorials, IO FAT evidence, onboarding, FAQ, field evidence, demo, issue intake and supply-chain contracts.")
print(
"ARSAS adoption and field-proof validation passed: 62 pages, 21 localized pages, "
"beginner learning, bilingual MMS, Reporting, GOOSE and SCL tutorials, IO FAT evidence, "
"onboarding, FAQ, field evidence, demo, issue intake and supply-chain contracts."
)
return 0


Expand Down
12 changes: 10 additions & 2 deletions scripts/verify-adoption-deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


def fetch(url: str) -> tuple[int, str]:
request = Request(url, headers={"User-Agent": "ARSAS-Adoption-Attestation/1.2", "Cache-Control": "no-cache"})
request = Request(url, headers={"User-Agent": "ARSAS-Adoption-Attestation/1.3", "Cache-Control": "no-cache"})
try:
with urlopen(request, timeout=25) as response:
return response.status, response.read().decode("utf-8", errors="replace")
Expand Down Expand Up @@ -39,6 +39,10 @@ def main() -> int:
"mms-client-iec61850.html": ("Takeaway 30 detik", "Add IED", "Functional Constraint", "Success criteria", "Bila gagal", "HowTo", "FAQPage", "mms-client.html"),
"smart-reporting.html": ("30-second takeaway", "DataSet", "BRCB", "URCB", "Success criteria", "If Reporting is silent", "HowTo", "FAQPage", "smart-reporting-iec61850.html"),
"smart-reporting-iec61850.html": ("Takeaway 30 detik", "DataSet", "BRCB", "URCB", "Success criteria", "Bila Reporting silent", "HowTo", "FAQPage", "smart-reporting.html"),
"goose-analyzer.html": ("30-second takeaway", "stNum", "sqNum", "Time Allowed to Live", "Success criteria", "If the result is wrong", "HowTo", "FAQPage", "analyzer-goose-iec61850.html"),
"analyzer-goose-iec61850.html": ("Takeaway 30 detik", "stNum", "sqNum", "Time Allowed to Live", "Success criteria", "Bila hasil salah", "HowTo", "FAQPage", "goose-analyzer.html"),
"scl-workspace.html": ("30-second takeaway", "ICD", "CID", "SCD", "IID", "Success criteria", "If the file is rejected", "HowTo", "FAQPage", "workspace-scl-iec61850.html"),
"workspace-scl-iec61850.html": ("Takeaway 30 detik", "ICD", "CID", "SCD", "IID", "Success criteria", "Bila file ditolak", "HowTo", "FAQPage", "scl-workspace.html"),
"quick-start.html": ("quick-step-number", "data-responsive-media=\"webp\"", "panduan-mulai-arsas.html", "io-list-fat-evidence.html"),
"io-list-fat-evidence.html": ("OFF → ON", "ON → OFF", "TestPointId", ".arsas", "Native PDF report", "bukti-fat-iolist-iec61850.html"),
"bukti-fat-iolist-iec61850.html": ("OFF → ON", "ON → OFF", "TestPointId", ".arsas", "Report PDF native", "io-list-fat-evidence.html"),
Expand Down Expand Up @@ -72,7 +76,11 @@ def main() -> int:
for error in errors:
print(f"- {error}")
return 1
print("Public ARSAS adoption attestation passed: Learning Center, beginner IEC 61850 guides, bilingual MMS and Reporting tutorials, Quick Start, IO FAT evidence, FAQ, compatibility, demo, filters and responsive media are live.")
print(
"Public ARSAS adoption attestation passed: Learning Center, beginner IEC 61850 guides, "
"bilingual MMS, Reporting, GOOSE and SCL tutorials, Quick Start, IO FAT evidence, FAQ, "
"compatibility, demo, filters and responsive media are live."
)
return 0


Expand Down
Loading