From d8be27a84ed312e4fca1a6a2247731978b0f0abb Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 12:33:55 -0700 Subject: [PATCH] lint: enforce frontmatter consistency across specs Add checks for: - 'organization' vs 'org' in author blocks (use 'org') - IPR must be 'noModificationTrust200902' - I-D.httpauth-payment target URL must be draft-ryan-httpauth-payment - independent submissions must have consensus: false Catches 19 violations on main across core, solana, card, stripe, tempo, lightning, stellar, discovery, MCP transport, and charge intent specs. --- scripts/lint_frontmatter.py | 17 +++ scripts/test_lint_frontmatter.py | 188 ++++++++++++++++++++++++++++--- 2 files changed, 191 insertions(+), 14 deletions(-) diff --git a/scripts/lint_frontmatter.py b/scripts/lint_frontmatter.py index c1da8950..d16725a9 100755 --- a/scripts/lint_frontmatter.py +++ b/scripts/lint_frontmatter.py @@ -9,7 +9,9 @@ REQUIRED_FIELDS = ["title", "abbrev", "docname", "version", "category", "ipr", "submissiontype", "consensus", "author"] AUTHOR_FIELD_ORDER = ["name", "ins", "email", "org"] +EXPECTED_IPR = "noModificationTrust200902" ID_HTTPAUTH_PAYMENT_TITLE = "The 'Payment' HTTP Authentication Scheme" +ID_HTTPAUTH_PAYMENT_TARGET = "https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/" def lint_file(path: Path) -> list[str]: @@ -23,6 +25,17 @@ def lint_file(path: Path) -> list[str]: if field not in meta: errors.append(f"missing required field '{field}'") + # Check IPR value + ipr = meta.get("ipr", "") + if ipr and ipr != EXPECTED_IPR: + errors.append(f"ipr should be '{EXPECTED_IPR}', got '{ipr}'") + + # Check consensus/submissiontype compatibility + submissiontype = meta.get("submissiontype", "") + consensus = meta.get("consensus") + if submissiontype == "independent" and consensus is True: + errors.append("consensus must be false for independent submissions") + # Check version format (two-digit string like "00", "01", … or int 0) version = meta.get("version") if not (isinstance(version, int) and 0 <= version <= 99) and not ( @@ -45,6 +58,8 @@ def lint_file(path: Path) -> list[str]: actual = [k for k in keys if k in AUTHOR_FIELD_ORDER] if actual != expected: errors.append(f"author[{i}] field order should be {expected}, got {actual}") + if "organization" in author: + errors.append(f"author[{i}] uses 'organization' instead of 'org'; use 'org' for consistency") # Check I-D.httpauth-payment reference format (if present) normative = meta.get("normative", {}) @@ -53,6 +68,8 @@ def lint_file(path: Path) -> list[str]: if ref: if "target" not in ref: errors.append("I-D.httpauth-payment missing 'target' field") + elif ref["target"] != ID_HTTPAUTH_PAYMENT_TARGET: + errors.append(f"I-D.httpauth-payment target should be '{ID_HTTPAUTH_PAYMENT_TARGET}', got '{ref['target']}'") title = ref.get("title", "") # Normalize title by stripping outer quotes for comparison normalized_title = title.strip('"').strip("'") diff --git a/scripts/test_lint_frontmatter.py b/scripts/test_lint_frontmatter.py index 84941e6b..11455ef7 100644 --- a/scripts/test_lint_frontmatter.py +++ b/scripts/test_lint_frontmatter.py @@ -26,7 +26,7 @@ def test_all_required_fields_present(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -47,7 +47,7 @@ def test_missing_required_field(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true --- @@ -65,7 +65,7 @@ def test_version_string_00(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -86,7 +86,7 @@ def test_version_int_0(self, tmp_path): docname: draft-test-00 version: 0 category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -107,7 +107,7 @@ def test_version_invalid(self, tmp_path): docname: draft-test-00 version: 1 category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -130,7 +130,7 @@ def test_title_capitalized(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -151,7 +151,7 @@ def test_title_lowercase_start(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -174,7 +174,7 @@ def test_correct_order(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -195,7 +195,7 @@ def test_wrong_order(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -210,6 +210,138 @@ def test_wrong_order(self, tmp_path): assert any("field order" in e for e in errors) +class TestIPR: + def test_correct_ipr(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert not any("ipr" in e for e in errors) + + def test_wrong_ipr(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: trust200902 +submissiontype: IETF +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert any("ipr should be 'noModificationTrust200902'" in e for e in errors) + + +class TestConsensusSubmissiontype: + def test_independent_with_consensus_false(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: independent +consensus: false +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert not any("consensus" in e for e in errors) + + def test_independent_with_consensus_true(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: independent +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert any("consensus must be false for independent" in e for e in errors) + + +class TestAuthorOrganization: + def test_org_is_correct(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert not any("organization" in e for e in errors) + + def test_organization_flagged(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + organization: Test Org +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert any("'organization' instead of 'org'" in e for e in errors) + + class TestIDReference: def test_valid_id_reference(self, tmp_path): content = """--- @@ -218,7 +350,7 @@ def test_valid_id_reference(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -229,7 +361,7 @@ def test_valid_id_reference(self, tmp_path): normative: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -246,7 +378,7 @@ def test_missing_target(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -273,7 +405,7 @@ def test_wrong_author_format(self, tmp_path): docname: draft-test-00 version: "00" category: info -ipr: trust200902 +ipr: noModificationTrust200902 submissiontype: IETF consensus: true author: @@ -284,7 +416,7 @@ def test_wrong_author_format(self, tmp_path): normative: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ author: - ins: J. Moxey date: 2026-01 @@ -293,3 +425,31 @@ def test_wrong_author_format(self, tmp_path): spec = write_spec(tmp_path, content) errors = lint_file(spec) assert any("should use 'name'" in e for e in errors) + + def test_wrong_target_url(self, tmp_path): + content = """--- +title: Test Spec +abbrev: Test +docname: draft-test-00 +version: "00" +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true +author: + - name: Test Author + ins: T. Author + email: test@example.com + org: Test Org +normative: + I-D.httpauth-payment: + title: "The 'Payment' HTTP Authentication Scheme" + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + author: + - name: Jake Moxey + date: 2026-01 +--- +""" + spec = write_spec(tmp_path, content) + errors = lint_file(spec) + assert any("target should be" in e for e in errors)