lead · Stage 5
時差と言語を越える非同期RFCを設計する
timezone、language、contextの非対称性を明示し、dissentと応答期限を記録したRFCでmeetingなしにレビュー可能な意思決定を作る。
到達目標
timezone、language、前提、応答期限をRFCへ埋め込み、非同期レビューのcontextを完成できる
- 時差、言語、文脈差を越える非同期RFCと決定ログ
- 言語能力やtimezoneを参加能力と誤認しないレビュー判断
異論を人格から分離して記録し、根拠あるresolutionをdecision logへ残せる
- 異論、応答、理由、期限、決定者を追跡できるdecision log
- consensusと全員一致を区別し、meeting不要な条件を説明する5分発表
timezone overlapだけを変え、同じproposal evidenceでmeetingなしのレビュー可能性を再評価できる
- 時差、言語、文脈差を越える非同期RFCと決定ログ
- timezone overlapを4時間から0時間へ変えた同一proposalの再構成
能力の進行
recognize
timezone、language、context、authorityの非対称性を識別できる
証拠: 時差、言語、文脈差を越える非同期RFCと決定ログ
explain
consensus、投票、全員一致、沈黙の違いを説明できる
証拠: consensusと全員一致を区別し、meeting不要な条件を説明する5分発表
apply
RFC section、応答期限、dissent responseを再現可能なdecision logへ組み立てられる
証拠: 時差、言語、文脈差を越える非同期RFCと決定ログ、異論、応答、理由、期限、決定者を追跡できるdecision log
diagnose
meeting依存、暗黙context、言語によるstatus差、異論の消去を反証できる
証拠: 言語能力やtimezoneを参加能力と誤認しないレビュー判断
lead
zero-overlapでも別のcontributorが判断根拠を再検証できる協働境界を運営できる
証拠: timezone overlapを4時間から0時間へ変えた同一proposalの再構成
なぜ重要か
世界規模の協働では、同じ時間に集まれる人、会議のlanguageを速く話せる人、背景contextをすでに知る人だけが意思決定を支配しやすい。timezone overlapが小さいほど、会議の開催をcommand successとして数える運営は、検証可能なsystem outcomeから離れる。
RFC 7282はrough consensusを単純な投票や全員一致から区別し、重大な異論を理解して扱うことを重視する。RFC 7322は読者が一貫して解釈できる明確な文書を求める。W3Cの18 August 2025 ProcessとCall for Consensusも、応答期間と異論の処理を意思決定の一部として扱う。
メンタルモデル
非同期RFCを、情報を配る文書ではなく、別の時間帯にいるcontributorが判断を独立再実行するinterfaceとして設計する。結論より先にproblem、alternatives、evidence、risksを置き、dissentとresponse-windowを閉じない。
| 観測 | 判断 | 必要な次の証拠 |
|---|---|---|
| contextと期限が完全 | 非同期reviewを継続 | dissentへのreasoned response |
| 暗黙の前提が残る | decisionを保留 | 用語、owner、制約、反証条件 |
| 重大なdissentが未処理 | consensusと呼ばない | 影響、代替案、採否理由 |
timezone overlapがなくてもdissentと採否理由を第三者が再評価できる記録は何か。
- 文脈
判断範囲、代替案、参加条件を文書へ揃える。
- Frame
problem、decision rights、対象外を固定する。
順序: 0
- Expose
alternatives、evidence、uncertaintyを同じ文書に置く。
順序: 1
- Include
timezone、working language、用語、UTC期限を示す。
順序: 2
- Frame
- レビュー
安全なresponse windowで異論と応答を追跡する。
- Invite
dissentを安全に記録するresponse-windowを開く。
順序: 3
- Resolve
異論ごとのresponseと採否理由を残す。
順序: 4
- Invite
- 決定
第三者がmeetingなしで再評価できる形にする。
- Replay
meeting unnecessaryな状態で第三者が判断を再評価する。
順序: 5
- Replay
decision rights、evidence、参加条件、response window、異論への応答を再生可能なdecision logへ結べる。
動く例で考える
timezone overlapを4時間から0時間へ変える
- 前提
- 2人のreviewerが同じsimulated proposalを評価する。能力や文化を推定せず、入力されたtimezoneとworking languageだけを参加設計に使う。
- 入力
- 7つのRFC section、proposal evidence、2人のparticipant、UTC response deadline、baseline 4時間とtransfer 0時間のoverlapを与える。
- 操作
- schemaとsection順をfail-closedで検証し、context completeness、dissent response、meeting necessityを同じ入力から導く。
- 観測
- overlapが0時間でもproposal evidence、working language、deadline、異論へのresponseが維持され、reviewはmeetingなしで完了する。
- 結論
- 文書生成のcommand successは合意というsystem outcomeではない。異論が記録され、addressedで、別のcontributorが理由を再評価できることが成果である。
python3.13 - <<'PY'
import json
HARNESS = "async_rfc_decision_lab_v1"
BASELINE_ASYNC_CONTEXT = {
"timezone_overlap_hours": 4,
"working_language": "plain-English",
}
TRANSFER_ASYNC_CONTEXT = {
"timezone_overlap_hours": 0,
"working_language": "plain-English",
}
PROPOSAL = {
"proposal_id": "rfc-async-deploy-window",
"problem": "release decision depends on a regional meeting",
"decision": "use an evidence-bound asynchronous review window",
"alternatives": [
"keep a rotating meeting",
"delegate without a review record",
],
"evidence": [
"same proposal snapshot",
"UTC response deadline",
"independent reviewer acknowledgement",
],
"risks": [
"missing implicit operational context",
"silence misread as agreement",
],
"dissent": {
"dissent_id": "dissent-rollback-authority",
"concern": "rollback authority is unclear",
"response": "name the incident commander and fallback owner",
"resolution": "accepted and added to the decision record",
},
"response_deadline_utc": "2026-08-03T09:00:00Z",
}
PARTICIPANTS = [
{
"participant_id": "reviewer-alpha",
"timezone": "UTC+09:00",
"working_language": "plain-English",
"acknowledged": True,
"availability": {
"baseline_local_hours": [9, 13],
"transferred_local_hours": [9, 13],
},
},
{
"participant_id": "reviewer-bravo",
"timezone": "UTC-07:00",
"working_language": "plain-English",
"acknowledged": True,
"availability": {
"baseline_local_hours": [17, 21],
"transferred_local_hours": [9, 13],
},
},
]
RFC_SECTION_ORDER = (
"problem",
"decision",
"alternatives",
"evidence",
"risks",
"dissent",
"response-window",
)
ASYNC_CONTEXT_FIELDS = {
"timezone_overlap_hours",
"working_language",
}
PROPOSAL_FIELDS = {
"proposal_id",
"problem",
"decision",
"alternatives",
"evidence",
"risks",
"dissent",
"response_deadline_utc",
}
DISSENT_FIELDS = {
"dissent_id",
"concern",
"response",
"resolution",
}
PARTICIPANT_FIELDS = {
"participant_id",
"timezone",
"working_language",
"acknowledged",
"availability",
}
AVAILABILITY_FIELDS = {
"baseline_local_hours",
"transferred_local_hours",
}
def is_number(value):
return type(value) in {int, float} and type(value) is not bool
def validate_async_context(context):
valid = (
type(context) is dict
and set(context) == ASYNC_CONTEXT_FIELDS
and is_number(context["timezone_overlap_hours"])
and 0 <= context["timezone_overlap_hours"] <= 24
and type(context["working_language"]) is str
and bool(context["working_language"])
)
if not valid:
raise AssertionError(
"async-transfer-invariant: invalid async context schema"
)
def changed_fields(baseline, transferred):
validate_async_context(baseline)
validate_async_context(transferred)
changed = [
field
for field in sorted(set(baseline) | set(transferred))
if baseline.get(field) != transferred.get(field)
]
if changed != ["timezone_overlap_hours"]:
raise AssertionError(
"async-transfer-invariant: transfer changed wrong input"
)
return changed
def validate_proposal(proposal):
valid = (
type(proposal) is dict
and set(proposal) == PROPOSAL_FIELDS
and all(
type(proposal[field]) is str and bool(proposal[field])
for field in {
"proposal_id",
"problem",
"decision",
"response_deadline_utc",
}
)
and all(
type(proposal[field]) is list
and bool(proposal[field])
and all(type(item) is str and bool(item) for item in proposal[field])
for field in {"alternatives", "evidence", "risks"}
)
and type(proposal["dissent"]) is dict
and set(proposal["dissent"]) == DISSENT_FIELDS
and all(
type(value) is str and bool(value)
for value in proposal["dissent"].values()
)
and proposal["response_deadline_utc"].endswith("Z")
)
if not valid:
raise AssertionError(
"async-input-invariant: invalid proposal schema"
)
def validate_participants(participants):
valid = (
type(participants) is list
and bool(participants)
and all(
type(participant) is dict
and set(participant) == PARTICIPANT_FIELDS
and all(
type(participant[field]) is str and bool(participant[field])
for field in {
"participant_id",
"timezone",
"working_language",
}
)
and type(participant["acknowledged"]) is bool
and type(participant["availability"]) is dict
and set(participant["availability"])
== AVAILABILITY_FIELDS
and all(
type(window) is list
and len(window) == 2
and all(type(hour) is int for hour in window)
and 0 <= window[0] < window[1] <= 24
for window in participant["availability"].values()
)
for participant in participants
)
and len(
{participant["participant_id"] for participant in participants}
) == len(participants)
)
if not valid:
raise AssertionError(
"async-input-invariant: invalid participant schema"
)
def timezone_offset_hours(timezone):
valid = (
type(timezone) is str
and len(timezone) == 9
and timezone.startswith("UTC")
and timezone[3] in {"+", "-"}
and timezone[6] == ":"
and timezone[4:6].isdigit()
and timezone[7:9] == "00"
)
if not valid:
raise AssertionError(
"async-timezone-invariant: invalid timezone"
)
magnitude = int(timezone[4:6])
if magnitude > 14:
raise AssertionError(
"async-timezone-invariant: timezone out of range"
)
return magnitude if timezone[3] == "+" else -magnitude
def derive_overlap_hours(participants, window_field):
utc_windows = []
for participant in participants:
offset = timezone_offset_hours(participant["timezone"])
local_window = participant["availability"][window_field]
utc_start = (local_window[0] - offset) % 24
utc_end = utc_start + (local_window[1] - local_window[0])
if not (0 <= utc_start < utc_end <= 24):
raise AssertionError(
"async-timezone-invariant: window crosses UTC day"
)
utc_windows.append([utc_start, utc_end])
overlap = min(window[1] for window in utc_windows) - max(
window[0] for window in utc_windows
)
return max(0, overlap)
def response_window_hours(overlap_hours):
return 24 if overlap_hours == 0 else 8
def validate_section_order():
expected = (
"problem",
"decision",
"alternatives",
"evidence",
"risks",
"dissent",
"response-window",
)
if RFC_SECTION_ORDER != expected:
raise AssertionError(
"async-order-invariant: invalid RFC section order"
)
def validate_all_inputs():
validate_async_context(BASELINE_ASYNC_CONTEXT)
validate_async_context(TRANSFER_ASYNC_CONTEXT)
validate_proposal(PROPOSAL)
validate_participants(PARTICIPANTS)
validate_section_order()
baseline_overlap = derive_overlap_hours(
PARTICIPANTS,
"baseline_local_hours",
)
transferred_overlap = derive_overlap_hours(
PARTICIPANTS,
"transferred_local_hours",
)
if (
baseline_overlap
!= BASELINE_ASYNC_CONTEXT["timezone_overlap_hours"]
or transferred_overlap
!= TRANSFER_ASYNC_CONTEXT["timezone_overlap_hours"]
):
raise AssertionError(
"async-timezone-invariant: overlap not availability-derived"
)
def evaluate_context_completeness(review_input):
proposal = review_input["proposal"]
participants = review_input["participants"]
context = review_input["context"]
complete = (
bool(proposal["problem"])
and bool(proposal["decision"])
and bool(proposal["alternatives"])
and bool(proposal["evidence"])
and bool(proposal["risks"])
and bool(proposal["response_deadline_utc"])
and bool(context["working_language"])
and all(
participant["acknowledged"]
and participant["working_language"]
== context["working_language"]
for participant in participants
)
)
return "complete" if complete else "incomplete"
def evaluate_dissent(review_input):
dissent = review_input["proposal"]["dissent"]
addressed = (
bool(dissent["concern"])
and bool(dissent["response"])
and bool(dissent["resolution"])
and "accepted" in dissent["resolution"]
)
return "addressed" if addressed else "unaddressed"
def evaluate_meeting_need(context_complete):
meeting_required = not context_complete
if meeting_required is not (not context_complete):
raise AssertionError(
"async-context-invariant: meeting derivation bypassed"
)
return meeting_required
def evaluate_review(
proposal,
participants,
context,
derived_overlap,
derived_response_window,
):
review_input = {
"proposal": proposal,
"participants": participants,
"context": context,
}
context_complete = evaluate_context_completeness(
review_input
)
if context_complete != "complete":
raise AssertionError(
"async-context-invariant: context derivation bypassed"
)
context_complete = context_complete == "complete"
dissent_addressed = evaluate_dissent(
review_input
)
if dissent_addressed != "addressed":
raise AssertionError(
"async-dissent-invariant: dissent derivation bypassed"
)
meeting_required = evaluate_meeting_need(context_complete)
if (
derived_overlap != context["timezone_overlap_hours"]
or derived_response_window
!= response_window_hours(derived_overlap)
or evaluate_meeting_need(False) is not True
):
raise AssertionError(
"async-timezone-invariant: async window derivation bypassed"
)
return {
"context_complete": context_complete,
"dissent_addressed": dissent_addressed == "addressed",
"meeting_required": meeting_required,
"derived_overlap_hours": derived_overlap,
"response_window_hours": derived_response_window,
}
def main():
validate_all_inputs()
changed = changed_fields(
BASELINE_ASYNC_CONTEXT,
TRANSFER_ASYNC_CONTEXT,
)
proposal = json.loads(
json.dumps(PROPOSAL, ensure_ascii=False)
)
transferred_proposal = json.loads(
json.dumps(PROPOSAL, ensure_ascii=False)
)
participants = json.loads(
json.dumps(PARTICIPANTS, ensure_ascii=False)
)
transferred_participants = json.loads(
json.dumps(PARTICIPANTS, ensure_ascii=False)
)
baseline_context = json.loads(
json.dumps(BASELINE_ASYNC_CONTEXT, ensure_ascii=False)
)
transferred_context = json.loads(
json.dumps(TRANSFER_ASYNC_CONTEXT, ensure_ascii=False)
)
validate_proposal(proposal)
validate_proposal(transferred_proposal)
validate_participants(participants)
validate_participants(transferred_participants)
baseline_overlap = derive_overlap_hours(
participants,
"baseline_local_hours",
)
transferred_overlap = derive_overlap_hours(
transferred_participants,
"transferred_local_hours",
)
baseline_response_window = response_window_hours(
baseline_overlap
)
transferred_response_window = response_window_hours(
transferred_overlap
)
baseline_review = evaluate_review(
proposal,
participants,
baseline_context,
baseline_overlap,
baseline_response_window,
)
transferred_review = evaluate_review(
transferred_proposal,
transferred_participants,
transferred_context,
transferred_overlap,
transferred_response_window,
)
same_proposal_evidence = (
proposal == transferred_proposal
and participants == transferred_participants
)
if not same_proposal_evidence:
raise AssertionError(
"async-transfer-invariant: proposal evidence drifted"
)
zero_overlap_review_completed = (
transferred_context["timezone_overlap_hours"] == 0
and transferred_review["context_complete"]
and transferred_review["dissent_addressed"]
and not transferred_review["meeting_required"]
)
if not zero_overlap_review_completed:
raise AssertionError(
"async-transfer-invariant: zero overlap review incomplete"
)
report = {
"harness": HARNESS,
"async_rfc": {
"context_complete": baseline_review["context_complete"],
"meeting_required": baseline_review["meeting_required"],
"meeting_need_derived_from_context": (
baseline_review["meeting_required"]
== (not baseline_review["context_complete"])
),
"derived_timezone_overlap_hours": baseline_overlap,
"response_window_hours": baseline_response_window,
"sections": list(RFC_SECTION_ORDER),
"timezone_context": baseline_context,
"language_context": baseline_context["working_language"],
"response_deadline": proposal["response_deadline_utc"],
},
"decision_log": {
"dissent_recorded": bool(proposal["dissent"]["concern"]),
"dissent_addressed": baseline_review["dissent_addressed"],
"reasoned_resolution": proposal["dissent"]["resolution"],
"reviewable_without_meeting": (
baseline_review["context_complete"]
and baseline_review["dissent_addressed"]
and not baseline_review["meeting_required"]
),
},
"timezone_overlap_transfer": {
"changed_assumption": "timezone-overlap",
"changed_fields": changed,
"same_proposal_evidence": same_proposal_evidence,
"zero_overlap_review_completed": zero_overlap_review_completed,
"derived_baseline_overlap_hours": baseline_overlap,
"derived_transferred_overlap_hours": transferred_overlap,
"baseline_response_window_hours": (
baseline_response_window
),
"transferred_response_window_hours": (
transferred_response_window
),
"baseline_context": baseline_context,
"transferred_context": transferred_context,
},
"fixture_metadata": {
"kind": "simulated",
"provenance": "lesson-defined asynchronous RFC fixture",
"limitations": (
"not evidence about any culture, language group, or person"
),
"synthetic_or_observed_explicit": True,
"hidden_assumption": (
"acknowledgement means the record was received, not agreement"
),
},
"input_derived_evidence": {
"proposal_id": proposal["proposal_id"],
"participant_count": len(participants),
"response_deadline": proposal["response_deadline_utc"],
},
"runtime_bound": {
"records": (
len(participants)
+ len(proposal["alternatives"])
+ len(proposal["evidence"])
+ len(proposal["risks"])
),
"subprocesses": 0,
"maximum_iterations": (
len(participants) + len(RFC_SECTION_ORDER)
),
},
"external_network_used": False,
"command_success_distinction": {
"command_completed": True,
"system_outcome_checked": (
baseline_review["context_complete"]
and baseline_review["dissent_addressed"]
),
"command_success_equals_system_outcome": False,
"outcome_evidence": {
"dissent_id": proposal["dissent"]["dissent_id"],
"reviewers": [
item["participant_id"] for item in participants
],
},
},
"mastery_evidence": {
"lab_steps": [
{"step": 1, "evidence": baseline_context},
{"step": 2, "evidence": list(RFC_SECTION_ORDER)},
{"step": 3, "evidence": proposal["dissent"]},
{"step": 4, "evidence": baseline_review},
{"step": 5, "evidence": transferred_review},
],
"assessments": [
{
"assessment": 1,
"evidence": "silence is not recorded as agreement",
},
{
"assessment": 2,
"evidence": baseline_review,
},
],
"rubric_dimensions": [
"technical-correctness",
"judgment",
"evidence",
"communication",
],
"transfer": {
"task": (
"timezone overlapだけを4時間から0時間へ変え、"
"同じproposalをmeetingなしでレビューできるRFCと"
"決定ログへ再構成する"
),
"changed_assumption": "timezone-overlap",
"evidence": {
"baseline": baseline_review,
"transferred": transferred_review,
},
},
"contributor_enablement": {
"other_contributor": "reviewer-bravo",
"review_evidence": proposal["evidence"],
},
},
}
print(json.dumps(report, ensure_ascii=False, separators=(",", ":")))
if __name__ == "__main__":
try:
main()
except AssertionError as error:
raise SystemExit(str(error)) from None
PY
トレードオフと失敗モード
非同期化は記録と参加機会を増やす一方、文書作成時間と応答latencyを増やす。すべてをRFCにせず、不可逆性、影響範囲、異論の可能性に応じて記録量とresponse-windowを調整する。
- 誤診: 返答がないのでconsensusである。反証: timezone、通知到達、language、期限理解を確認し、沈黙を賛成票に変換しない。
- 誤診: meetingで全員が頷いたので判断はreview可能である。反証: alternatives、evidence、dissent responseが記録されなければ第三者は独立再評価できない。
- 失敗モード: plain languageを情報量の削減と取り違える。用語を定義し、制約とuncertaintyを残したまま文を短くする。
- 失敗モード: すべてのdissentを解消済みに見せる。未解決の懸念、owner、観測条件、再検討日をdecision logに保持する。
知識チェック
- 4時間のtimezone overlapを0時間へ変えるとき、proposal evidenceまで変えてはいけないのはなぜか。
- Call for Consensusのresponse-windowと沈黙による同意はどう違うか。
- meeting unnecessaryでもsystem outcomeを確認できる証拠を3つ挙げよ。
- dissentがrecordedだがaddressedではない具体例を作り、次のactionを説明せよ。
出典と次の学習
一次規範としてRFC 7282、RFC 7322、W3C 18 August 2025 Processを読み、W3C GuidebookのThe Art of ConsensusでCall for Consensusと異論処理の実務を確認する。
次に、自分の直近のdesign decisionをこの7 sectionへ再構成する。別timezoneのcontributorに同期説明をせず、曖昧なcontext、未処理のdissent、期限の解釈差をreview evidenceとして返してもらう。
実践ラボ
4時間のtimezone overlapを0時間へ変えてRFCを再評価する
提出成果物: 時差、言語、文脈差を越える非同期RFCと決定ログ
- proposal、participant、timezone、working languageのfixtureと限界を固定する
- problemからresponse-windowまでのRFC sectionと応答期限を構成する
- dissentを記録し、evidenceに基づくresponseとreasoned resolutionを残す
- meetingの成否ではなくcontext completenessとreview evidenceを検証する
- timezone overlapだけを4時間から0時間へ変えて同じproposalを再評価する
説明して理解を確かめる
5分で、RFC 7282のrough consensus、RFC 7322の明確な文書、W3CのCall for Consensusを、全員一致や沈黙の同意ではなく検証可能な非同期判断へ結ぶ。
アセスメント
問い: 期限まで返答がないparticipantを賛成として数えてよいか。
期待する証拠: timezone、通知到達、language context、response deadline、明示的dissent、decision rule
問い: meetingでは合意したがalternativesと異論へのresponseが残っていない。system outcomeをどう判定するか。
期待する証拠: command successとの区別、context completeness、reasoned resolution、reviewable record
別問題へ転用する
timezone overlapだけを4時間から0時間へ変え、同じproposalをmeetingなしでレビューできるRFCと決定ログへ再構成する
復習スケジュール
- 1日後
meetingを開かずにcontext completenessを判定する最小証拠は何か
- 7日後
dissentを記録しただけではaddressedにならない理由は何か
- 30日後
timezone overlapが0時間のときresponse-windowをどう設計するか
- 90日後
meetingを開かずにcontext completenessを判定する最小証拠は何か
評価ルーブリック
| 観点 | 未達 | 発展途上 | 熟達 | 卓越 |
|---|---|---|---|---|
| technical-correctness | 結論だけを書き、前提、alternatives、期限がない | sectionはあるがtimezone、language、dissentが判断へ結び付かない | 全section、participant context、応答期限、異論とresponseを同じ入力から導く | 順序、schema、changed fieldを検証し、zero-overlapでも同じ結果を再現できる |
| judgment | meeting参加または沈黙を合意とみなす | 異論を集めるが重要度やresponseを記録しない | 重大な異論を根拠で解消または未解決としてdecision logへ残す | 文化、言語、権限の非対称性を補正し、少数意見から決定を更新できる |
| evidence | 会議を実施した事実だけを示す | 文書はあるがinput provenanceとdecision ruleがない | proposal、alternatives、evidence、dissent、response、deadlineを監査可能に残す | 別timezoneのcontributorが同期説明なしで判断を独立再評価できる |
| communication | 暗黙知、略語、local timeだけで伝える | 平易だが読者、用語、期限の基準が曖昧である | plain language、UTC基準、用語、owner、response-windowを明示する | 翻訳可能性と心理的安全性を保ち、異論が意思決定品質を上げる |
出典
以下の外部資料は利用者が選択したときだけ開きます。
- RFC 7282: On Consensus and Humming in the IETF (standard)
- RFC 7322: RFC Style Guide (standard)
- W3C Process Document, 18 August 2025 (standard)
- The Art of Consensus (W3C Guidebook, accessed 2026-07-31) (primary)