@ccoalm/ccl-skills 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +11 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-unverified-cli-flag.sh +309 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_unverified_cli_flag.sh +483 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +5 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +10 -8
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +1 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +1 -1
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +2 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +1 -1
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +14 -11
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +2 -2
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +5 -1
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +13 -11
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +72 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +1 -1
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +25 -9
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +1 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +1 -1
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +16 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +7 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +11 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -10
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +4 -5
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +1 -1
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +7 -1
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -5
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +5 -1
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +1 -1
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/audit-history-architecture.md +31 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +1 -1
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +7 -4
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +2 -2
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/notification-architecture.md +28 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +1 -1
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/replay-comparison-architecture.md +28 -0
  78. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/workflow-state-architecture.md +39 -0
  79. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +10 -7
  80. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +8 -0
  81. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/audit-history-patterns.md +29 -0
  82. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +16 -0
  83. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +25 -1
  84. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/notification-patterns.md +40 -0
  85. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +1 -1
  86. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/replay-comparison-patterns.md +30 -0
  87. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +48 -0
  88. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +10 -1
  89. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +2 -0
  90. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
  91. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
  92. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +142 -4
  93. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +21 -2
  94. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
  95. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  96. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +5 -4
  97. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
  98. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +69 -0
  99. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +10 -0
  100. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
  101. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
  102. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +93 -2
  103. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-parallel-stack-parity.sh +119 -0
  104. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
  105. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
  106. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
  107. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
  108. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
  109. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_parallel_stack_parity.sh +183 -0
  110. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +19 -0
  111. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
  112. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
  113. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
  114. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
  115. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
  116. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
  117. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
  118. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
  119. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
  120. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
  121. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
  122. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
  123. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
  124. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
  125. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
  126. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
  127. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +9 -6
  128. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +11 -11
  129. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
  130. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +16 -0
  131. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +1 -1
  132. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +16 -5
  133. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +5 -3
  134. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +16 -6
  135. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/self-benchmark-baseline.md +37 -0
  136. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +7 -5
  137. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
  138. package/dist/assets/release.json +275 -105
  139. package/package.json +1 -1
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env python3
2
+ """Append or compact bounded intent without lossy truncation.
3
+
4
+ The caller owns writer serialization and provides a trusted, stable parent
5
+ directory. ``--expected-sha256`` rejects input that was already stale when
6
+ opened; it is not a lock against concurrent writers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import base64
13
+ import hashlib
14
+ import json
15
+ import os
16
+ import re
17
+ import stat
18
+ import sys
19
+ import tempfile
20
+ from pathlib import Path
21
+ from typing import NoReturn
22
+
23
+
24
+ MIN_INTENT_CHARS = 8
25
+ MAX_INTENT_CHARS = 4000
26
+ MAX_PLAN_BYTES = 32_000
27
+ PLAN_FIELDS = {"intent", "acceptance", "self_review", "evidence"}
28
+ SHA256_RE = re.compile(r"[0-9a-f]{64}")
29
+ STABLE_CORE_EVIDENCE_ID = "review-plan-intent-stable-core-v1"
30
+ STABLE_CORE_RESULT_RE = re.compile(r"chars=([1-9][0-9]{0,3});sha256=([0-9a-f]{64})")
31
+ HISTORY_EVIDENCE_PREFIX = "review-plan-intent-history-v1"
32
+ HISTORY_BASE64_CHUNK_CHARS = 1800
33
+ MAX_EVIDENCE_ROWS = 50
34
+
35
+
36
+ class UpdateError(Exception):
37
+ def __init__(self, reason: str, detail: str) -> None:
38
+ super().__init__(detail)
39
+ self.reason = reason
40
+ self.detail = detail
41
+
42
+
43
+ class DuplicateKeyError(ValueError):
44
+ pass
45
+
46
+
47
+ def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
48
+ result: dict[str, object] = {}
49
+ for key, value in pairs:
50
+ if key in result:
51
+ raise DuplicateKeyError
52
+ result[key] = value
53
+ return result
54
+
55
+
56
+ def fail(reason: str, detail: str) -> NoReturn:
57
+ raise UpdateError(reason, detail)
58
+
59
+
60
+ def read_regular(path: Path, *, label: str, max_bytes: int) -> tuple[bytes, int]:
61
+ if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_NONBLOCK"):
62
+ fail(
63
+ f"{label}_unsupported",
64
+ "this platform cannot safely open files without following links or blocking on special files",
65
+ )
66
+ flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | getattr(os, "O_CLOEXEC", 0)
67
+ fd = -1
68
+ try:
69
+ fd = os.open(path, flags)
70
+ except OSError as exc:
71
+ fail(f"{label}_unreadable", str(exc))
72
+ try:
73
+ info = os.fstat(fd)
74
+ if not stat.S_ISREG(info.st_mode):
75
+ fail(f"{label}_not_regular", "expected a regular, non-symlink file")
76
+ if info.st_nlink != 1:
77
+ fail(f"{label}_hardlinked", "refusing a multiply-linked file")
78
+ if label == "plan" and hasattr(os, "geteuid") and info.st_uid != os.geteuid():
79
+ fail("plan_not_owned", "the plan must be owned by the current user")
80
+ if info.st_size > max_bytes:
81
+ fail(f"{label}_too_large", f"maximum is {max_bytes} bytes")
82
+ with os.fdopen(fd, "rb") as handle:
83
+ fd = -1
84
+ chunks: list[bytes] = []
85
+ remaining = max_bytes + 1
86
+ while remaining:
87
+ chunk = handle.read(min(65_536, remaining))
88
+ if not chunk:
89
+ break
90
+ chunks.append(chunk)
91
+ remaining -= len(chunk)
92
+ data = b"".join(chunks)
93
+ except OSError as exc:
94
+ fail(f"{label}_unreadable", str(exc))
95
+ finally:
96
+ if fd >= 0:
97
+ os.close(fd)
98
+ if len(data) > max_bytes:
99
+ fail(f"{label}_too_large", f"maximum is {max_bytes} bytes")
100
+ return data, stat.S_IMODE(info.st_mode)
101
+
102
+
103
+ def decode_utf8(data: bytes, *, label: str) -> str:
104
+ try:
105
+ return data.decode("utf-8")
106
+ except UnicodeDecodeError as exc:
107
+ fail(f"{label}_invalid_utf8", f"invalid UTF-8 at byte {exc.start}")
108
+
109
+
110
+ def digest(data: bytes) -> str:
111
+ return hashlib.sha256(data).hexdigest()
112
+
113
+
114
+ def normalized_intent(
115
+ value: object, *, reason_prefix: str, minimum: int = MIN_INTENT_CHARS
116
+ ) -> str:
117
+ if not isinstance(value, str):
118
+ fail(f"{reason_prefix}_not_string", "intent must be a string")
119
+ stripped = value.strip()
120
+ if value != stripped:
121
+ fail(f"{reason_prefix}_not_normalized", "intent must not have outer whitespace")
122
+ if len(value) < minimum:
123
+ fail(f"{reason_prefix}_too_short", f"minimum is {minimum} characters")
124
+ return value
125
+
126
+
127
+ def remove_file_line_ending(value: str) -> str:
128
+ """Remove only the one line ending used as a text-file delimiter."""
129
+ if value.endswith("\r\n"):
130
+ return value[:-2]
131
+ if value.endswith(("\n", "\r")):
132
+ return value[:-1]
133
+ return value
134
+
135
+
136
+ def render_plan(plan: dict[str, object]) -> bytes:
137
+ try:
138
+ encoded = (json.dumps(plan, ensure_ascii=False, indent=2) + "\n").encode(
139
+ "utf-8"
140
+ )
141
+ except UnicodeEncodeError as exc:
142
+ fail("updated_plan_invalid_unicode", f"cannot encode UTF-8 at character {exc.start}")
143
+ if len(encoded) > MAX_PLAN_BYTES:
144
+ fail("updated_plan_too_large", f"maximum is {MAX_PLAN_BYTES} bytes")
145
+ return encoded
146
+
147
+
148
+ def stable_core_identity(plan: dict[str, object]) -> tuple[int, str]:
149
+ evidence = plan.get("evidence")
150
+ if not isinstance(evidence, list):
151
+ fail("intent_core_identity_missing", "plan evidence has no stable-core identity")
152
+ records = [
153
+ item
154
+ for item in evidence
155
+ if isinstance(item, dict) and item.get("id") == STABLE_CORE_EVIDENCE_ID
156
+ ]
157
+ if not records:
158
+ fail("intent_core_identity_missing", "plan evidence has no stable-core identity")
159
+ if len(records) != 1:
160
+ fail("intent_core_identity_invalid", "plan must contain exactly one stable-core identity")
161
+ record = records[0]
162
+ if set(record) != {"id", "result"} or not isinstance(record.get("result"), str):
163
+ fail("intent_core_identity_invalid", "stable-core identity has an invalid schema")
164
+ match = STABLE_CORE_RESULT_RE.fullmatch(record["result"])
165
+ if match is None:
166
+ fail("intent_core_identity_invalid", "stable-core identity has an invalid encoding")
167
+ core_chars = int(match.group(1))
168
+ if not MIN_INTENT_CHARS <= core_chars <= MAX_INTENT_CHARS:
169
+ fail(
170
+ "intent_core_identity_invalid",
171
+ f"stable-core character length must be between {MIN_INTENT_CHARS} and {MAX_INTENT_CHARS}",
172
+ )
173
+ return core_chars, match.group(2)
174
+
175
+
176
+ def text_digest(value: str, *, reason: str, label: str) -> str:
177
+ try:
178
+ return digest(value.encode("utf-8"))
179
+ except UnicodeEncodeError as exc:
180
+ fail(reason, f"{label} cannot encode as UTF-8 at character {exc.start}")
181
+
182
+
183
+ def archive_discarded_intent(
184
+ plan: dict[str, object], *, old_intent: str, core: str
185
+ ) -> None:
186
+ """Retain the exact old intent bytes before compacting its visible field."""
187
+ suffix = old_intent[len(core) :]
188
+ if not suffix:
189
+ return
190
+ evidence = plan.get("evidence")
191
+ if not isinstance(evidence, list):
192
+ fail("intent_history_invalid", "plan evidence must be an array")
193
+ used_ids = {
194
+ item.get("id")
195
+ for item in evidence
196
+ if isinstance(item, dict) and isinstance(item.get("id"), str)
197
+ }
198
+ group = None
199
+ for group_number in range(1, MAX_EVIDENCE_ROWS + 1):
200
+ candidate_group = f"{group_number:04d}"
201
+ candidate_prefix = f"{HISTORY_EVIDENCE_PREFIX}-{candidate_group}"
202
+ if not any(
203
+ evidence_id == f"{candidate_prefix}-manifest"
204
+ or evidence_id.startswith(f"{candidate_prefix}-part-")
205
+ for evidence_id in used_ids
206
+ ):
207
+ group = candidate_group
208
+ break
209
+ if group is None:
210
+ fail("intent_history_full", "no unique intent-history evidence group remains")
211
+ try:
212
+ old_bytes = old_intent.encode("utf-8")
213
+ suffix_bytes = suffix.encode("utf-8")
214
+ except UnicodeEncodeError as exc:
215
+ fail(
216
+ "intent_history_invalid_unicode",
217
+ f"old intent cannot encode as UTF-8 at character {exc.start}",
218
+ )
219
+ encoded_suffix = base64.b64encode(suffix_bytes).decode("ascii")
220
+ chunks = [
221
+ encoded_suffix[index : index + HISTORY_BASE64_CHUNK_CHARS]
222
+ for index in range(0, len(encoded_suffix), HISTORY_BASE64_CHUNK_CHARS)
223
+ ]
224
+ prefix = f"{HISTORY_EVIDENCE_PREFIX}-{group}"
225
+ rows: list[dict[str, str]] = [
226
+ {
227
+ "id": f"{prefix}-manifest",
228
+ "result": (
229
+ f"format={HISTORY_EVIDENCE_PREFIX};"
230
+ f"old_chars={len(old_intent)};"
231
+ f"old_sha256={hashlib.sha256(old_bytes).hexdigest()};"
232
+ f"core_chars={len(core)};"
233
+ f"suffix_bytes={len(suffix_bytes)};"
234
+ f"suffix_sha256={hashlib.sha256(suffix_bytes).hexdigest()};"
235
+ f"encoding=base64-utf8;parts={len(chunks)}"
236
+ ),
237
+ }
238
+ ]
239
+ rows.extend(
240
+ {
241
+ "id": f"{prefix}-part-{part_number:04d}",
242
+ "result": (
243
+ f"format={HISTORY_EVIDENCE_PREFIX};group={group};"
244
+ f"part={part_number}/{len(chunks)};data={chunk}"
245
+ ),
246
+ }
247
+ for part_number, chunk in enumerate(chunks, start=1)
248
+ )
249
+ if len(evidence) + len(rows) > MAX_EVIDENCE_ROWS:
250
+ fail(
251
+ "intent_history_evidence_overflow",
252
+ f"zero-loss compaction needs {len(rows)} history rows but the plan "
253
+ f"would exceed {MAX_EVIDENCE_ROWS} evidence rows",
254
+ )
255
+ plan["evidence"] = [*evidence, *rows]
256
+
257
+
258
+ def atomic_replace(path: Path, data: bytes, mode: int) -> None:
259
+ fd = -1
260
+ temp_path: Path | None = None
261
+ try:
262
+ try:
263
+ fd, raw_temp = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
264
+ temp_path = Path(raw_temp)
265
+ with os.fdopen(fd, "wb") as handle:
266
+ fd = -1
267
+ handle.write(data)
268
+ handle.flush()
269
+ os.fchmod(handle.fileno(), mode)
270
+ os.fsync(handle.fileno())
271
+ except OSError as exc:
272
+ fail("plan_write_failed", str(exc))
273
+ try:
274
+ os.replace(temp_path, path)
275
+ temp_path = None
276
+ except OSError as exc:
277
+ fail("plan_write_failed", str(exc))
278
+ try:
279
+ directory_flags = (
280
+ os.O_RDONLY
281
+ | getattr(os, "O_DIRECTORY", 0)
282
+ | getattr(os, "O_CLOEXEC", 0)
283
+ )
284
+ directory_fd = os.open(path.parent, directory_flags)
285
+ try:
286
+ os.fsync(directory_fd)
287
+ finally:
288
+ os.close(directory_fd)
289
+ except OSError as exc:
290
+ fail(
291
+ "plan_committed_durability_unknown",
292
+ f"the target was replaced with sha256={digest(data)} but directory sync failed ({exc}); re-read the plan and do not retry blindly",
293
+ )
294
+ finally:
295
+ cleanup_errors: list[str] = []
296
+ if fd >= 0:
297
+ try:
298
+ os.close(fd)
299
+ except OSError as exc:
300
+ cleanup_errors.append(f"close failed: {exc}")
301
+ if temp_path is not None:
302
+ try:
303
+ temp_path.unlink()
304
+ except FileNotFoundError:
305
+ pass
306
+ except OSError as exc:
307
+ cleanup_errors.append(f"temporary-file removal failed: {exc}")
308
+ if cleanup_errors:
309
+ active_error = sys.exc_info()[1]
310
+ detail = "; ".join(cleanup_errors)
311
+ if isinstance(active_error, UpdateError):
312
+ detail = (
313
+ f"{detail}; original failure was {active_error.reason}: "
314
+ f"{active_error.detail}"
315
+ )
316
+ raise UpdateError("plan_cleanup_failed", detail)
317
+
318
+
319
+ def parse_args(argv: list[str]) -> argparse.Namespace:
320
+ parser = argparse.ArgumentParser(
321
+ description=(
322
+ "Update a review plan intent without lossy truncation. Overflow is "
323
+ "an error and leaves the plan unchanged."
324
+ )
325
+ )
326
+ parser.add_argument("--plan", required=True, type=Path)
327
+ parser.add_argument("--append-intent-file", type=Path)
328
+ parser.add_argument("--compact-core-intent-file", type=Path)
329
+ parser.add_argument("--latest-intent-file", type=Path)
330
+ parser.add_argument(
331
+ "--expected-sha256",
332
+ help="optional stale-input guard for the current plan bytes",
333
+ )
334
+ return parser.parse_args(argv)
335
+
336
+
337
+ def run(argv: list[str]) -> int:
338
+ args = parse_args(argv)
339
+ append_mode = args.append_intent_file is not None
340
+ compact_mode = (
341
+ args.compact_core_intent_file is not None or args.latest_intent_file is not None
342
+ )
343
+ if append_mode == compact_mode or (
344
+ compact_mode
345
+ and (
346
+ args.compact_core_intent_file is None
347
+ or args.latest_intent_file is None
348
+ )
349
+ ):
350
+ fail(
351
+ "intent_update_mode_invalid",
352
+ "choose append, or supply both compact core and latest intent files",
353
+ )
354
+ plan_bytes, plan_mode = read_regular(
355
+ args.plan, label="plan", max_bytes=MAX_PLAN_BYTES
356
+ )
357
+ if plan_mode & 0o7000:
358
+ fail("plan_mode_unsupported", "setuid, setgid, and sticky plan modes are unsupported")
359
+ plan_mode &= 0o777
360
+ current_digest = digest(plan_bytes)
361
+ if args.expected_sha256 is not None:
362
+ expected = args.expected_sha256.lower()
363
+ if SHA256_RE.fullmatch(expected) is None:
364
+ fail("expected_sha256_invalid", "expected exactly 64 hexadecimal characters")
365
+ if expected != current_digest:
366
+ fail("plan_digest_mismatch", "the plan changed after the caller read it")
367
+
368
+ try:
369
+ plan = json.loads(
370
+ decode_utf8(plan_bytes, label="plan"),
371
+ object_pairs_hook=reject_duplicate_keys,
372
+ )
373
+ except DuplicateKeyError:
374
+ fail("plan_duplicate_key", "duplicate JSON object keys are unsupported")
375
+ except json.JSONDecodeError as exc:
376
+ fail("plan_invalid_json", f"JSON parse failed at line {exc.lineno} column {exc.colno}")
377
+ if not isinstance(plan, dict) or set(plan) != PLAN_FIELDS:
378
+ fail("plan_schema_invalid", "top-level fields must be intent, acceptance, self_review, evidence")
379
+ old_intent = normalized_intent(plan["intent"], reason_prefix="plan_intent")
380
+
381
+ if append_mode:
382
+ if len(old_intent) > MAX_INTENT_CHARS:
383
+ fail("plan_intent_overflow", f"maximum is {MAX_INTENT_CHARS} characters")
384
+ incoming_bytes, _ = read_regular(
385
+ args.append_intent_file, label="intent_input", max_bytes=MAX_PLAN_BYTES
386
+ )
387
+ incoming = remove_file_line_ending(
388
+ decode_utf8(incoming_bytes, label="intent_input")
389
+ )
390
+ if not incoming:
391
+ fail("intent_append_empty", "append input must not be empty")
392
+ candidate = old_intent + incoming
393
+ reason_prefix = "intent_append"
394
+ else:
395
+ core_bytes, _ = read_regular(
396
+ args.compact_core_intent_file,
397
+ label="intent_core",
398
+ max_bytes=MAX_PLAN_BYTES,
399
+ )
400
+ latest_bytes, _ = read_regular(
401
+ args.latest_intent_file,
402
+ label="intent_latest",
403
+ max_bytes=MAX_PLAN_BYTES,
404
+ )
405
+ core = normalized_intent(
406
+ remove_file_line_ending(decode_utf8(core_bytes, label="intent_core")),
407
+ reason_prefix="intent_core",
408
+ )
409
+ latest = normalized_intent(
410
+ remove_file_line_ending(decode_utf8(latest_bytes, label="intent_latest")),
411
+ reason_prefix="intent_latest",
412
+ minimum=1,
413
+ )
414
+ if not old_intent.startswith(core):
415
+ fail(
416
+ "intent_core_not_preserved",
417
+ "compact core must be an exact prefix of the current intent",
418
+ )
419
+ if latest in old_intent:
420
+ fail(
421
+ "intent_latest_not_new",
422
+ "latest transition is already present in the current intent",
423
+ )
424
+ core_chars, core_sha256 = stable_core_identity(plan)
425
+ if len(old_intent) < core_chars:
426
+ fail(
427
+ "plan_core_identity_mismatch",
428
+ "current intent is shorter than its persisted stable-core identity",
429
+ )
430
+ persisted_core = old_intent[:core_chars]
431
+ if text_digest(
432
+ persisted_core,
433
+ reason="plan_core_identity_mismatch",
434
+ label="persisted stable core",
435
+ ) != core_sha256:
436
+ fail(
437
+ "plan_core_identity_mismatch",
438
+ "persisted stable-core identity does not match the current intent",
439
+ )
440
+ if len(core) != core_chars or text_digest(
441
+ core,
442
+ reason="intent_core_identity_mismatch",
443
+ label="compact core",
444
+ ) != core_sha256:
445
+ fail(
446
+ "intent_core_identity_mismatch",
447
+ "compact core does not match the plan's persisted stable-core identity",
448
+ )
449
+ archive_discarded_intent(plan, old_intent=old_intent, core=core)
450
+ candidate = f"{core}\n\n{latest}"
451
+ reason_prefix = "intent_compact"
452
+
453
+ candidate = normalized_intent(candidate, reason_prefix=reason_prefix)
454
+ if len(candidate) > MAX_INTENT_CHARS:
455
+ fail(
456
+ f"{reason_prefix}_overflow",
457
+ f"candidate has {len(candidate)} characters; maximum is {MAX_INTENT_CHARS}; rebuild intent as core + latest and keep history in evidence/prior results",
458
+ )
459
+
460
+ plan["intent"] = candidate
461
+ updated = render_plan(plan)
462
+ atomic_replace(args.plan, updated, plan_mode)
463
+ # flush inside the guarded path: without it the receipt sits in the stdout
464
+ # buffer and a closed pipe only surfaces BrokenPipeError at interpreter
465
+ # shutdown, after main() returned rc 0 and past its handler.
466
+ print(
467
+ "review_plan_intent_updated "
468
+ f"mode={'append' if append_mode else 'compact'} "
469
+ f"chars={len(candidate)} old_sha256={current_digest} new_sha256={digest(updated)}",
470
+ flush=True,
471
+ )
472
+ return 0
473
+
474
+
475
+ def main() -> int:
476
+ try:
477
+ return run(sys.argv[1:])
478
+ except UpdateError as exc:
479
+ print(
480
+ f"review_plan_intent_error: reason={exc.reason} detail={exc.detail}",
481
+ file=sys.stderr,
482
+ )
483
+ return 2
484
+ except (BrokenPipeError, OSError):
485
+ # The success receipt is printed (flushing) only after atomic_replace
486
+ # committed, and every other file operation converts its OSError to
487
+ # UpdateError, so an OS-level error here means the receipt was lost on
488
+ # a closed/broken stdout — not that the update failed. rc 0 with no
489
+ # receipt would read as "no update happened"; report the committed-but-
490
+ # unreported state the same way a failed durability sync does.
491
+ try:
492
+ print(
493
+ "review_plan_intent_error: reason=plan_committed_receipt_lost "
494
+ "detail=stdout closed before the success receipt was delivered; "
495
+ "re-read the plan for the committed state and do not retry blindly",
496
+ file=sys.stderr,
497
+ )
498
+ sys.stderr.flush()
499
+ except (BrokenPipeError, OSError):
500
+ pass
501
+ # Point stdout at devnull so the interpreter's shutdown flush of the
502
+ # broken pipe cannot override this exit status with 120.
503
+ try:
504
+ devnull = os.open(os.devnull, os.O_WRONLY)
505
+ os.dup2(devnull, sys.stdout.fileno())
506
+ os.close(devnull)
507
+ except (OSError, ValueError):
508
+ pass
509
+ return 2
510
+
511
+
512
+ if __name__ == "__main__":
513
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: go-microservice-architecture
3
- description: Use when designing, reviewing, or explaining a new product's Go backend or microservice architecture using Kitex or similar RPC, Hertz or similar HTTP gateway, protobuf IDL, MySQL/GORM-style relational storage, Redis cache/locks/rate limiting, service discovery, dynamic config, message queues, observability, DI, and code generation. Product-agnostic; do not depend on existing codebase paths, service names, or legacy repositories. Prefer this for architecture, service boundaries, contracts, data ownership, reliability, security, and platform decisions when no code changes are requested; use go-microservice-dev for implementation work. Triggers also include "Go 后端架构怎么设计", "Go 微服务怎么拆", "RPC 接口怎么定义", "Go 服务边界", "Go 服务重构 / 架构分层重构", "拆分 Go 服务的上帝类/模块边界", "decompose a god class in a Go service".
3
+ description: Design, review, or explain a new product's Go backend/microservice architecture using Kitex or similar RPC, Hertz or similar HTTP gateway, protobuf IDL, MySQL/GORM storage, Redis cache/locks/limits, service discovery, dynamic config, message queues, observability, DI, and code generation. Also owns multi-tenant isolation, event-driven/Kafka, and data-platform (sharding/replicas/HA) architecture. Product-agnostic; no dependence on existing codebase paths, service names, or legacy repos. Prefer this for architecture/boundary/contract/data-ownership/reliability/security/platform decisions with no code changes; use go-microservice-dev for implementation. Triggers include "Go 后端架构怎么设计", "Go 微服务怎么拆", "RPC 接口怎么定义", "Go 服务边界", "Go 服务重构 / 架构分层重构", "拆分 Go 服务的上帝类/模块边界 / decompose a god class".
4
4
  ---
5
5
 
6
6
  # Go Microservice Architecture
@@ -101,6 +101,8 @@ Not appropriate for:
101
101
 
102
102
  ## Dependency Direction
103
103
 
104
+ This layering is the **[Ports-and-Adapters / Hexagonal](https://alistair.cockburn.us/hexagonal-architecture/)** idea in moderation (Alistair Cockburn; borrowed scope: the ports/adapters placement idea only, not the full pattern vocabulary) — infrastructure adapters sit behind interfaces the inner layers own.
105
+
104
106
  Recommended:
105
107
 
106
108
  ```text
@@ -4,7 +4,7 @@ Use when designing the data-platform substrate of a service or service-fleet: DB
4
4
 
5
5
  This complements `data-modeling-and-migrations.md` (which owns schema, index, transaction, outbox, and per-service migration concerns): this file owns the **substrate** that schema and queries sit on. Load both when designing a new data-bound service or auditing an existing one.
6
6
 
7
- > **Sibling sync.** A parallel `python-service-architecture/references/data-platform-architecture.md` mirrors **all non-stack-specific sections** of this file. Only the *Go-specific implementation patterns* section diverges by stack. The mirrored sections stay free of three categories of stack-specific token: DB-engine-specific syntax, runtime/concurrency-mechanic names, and library/framework API names. The concrete token list and grep command live in the *Mirrored-section grep gate* subsection at the end of this file's stack-glue.
7
+ > **Sibling sync.** A parallel `python-service-architecture/references/data-platform-architecture.md` mirrors **all non-stack-specific sections** of this file. Only the *Go-specific implementation patterns* section diverges by stack. The mirrored sections stay free of three categories of stack-specific token: DB-engine-specific syntax, runtime/concurrency-mechanic names, and library/framework API names. The concrete token list and grep command live in the *Mirrored-section grep gate* subsection at the end of this file's stack-glue. Tree-specific routing references are written inline for both trees so the mirrored bytes stay identical; cross-file parity is machine-checked by `skill-extraction-workflow/scripts/check-parallel-stack-parity.sh` (wired into `check-ccl-skills.sh`), which diffs the mirrored regions byte-for-byte (no normalization) and blocks on any divergence.
8
8
 
9
9
  > **Sanitization boundary.** Vendor names (PostgreSQL, MySQL, Vitess, TiDB, CockroachDB, Aurora, Cloud Spanner, AlloyDB, Cloud SQL, DynamoDB, RDS Proxy, PgBouncer, ProxySQL, S3, Glacier, gp3, io2, etc.) below are illustrative; concrete topology choices, region names, cluster identifiers, and capacity numbers live only in the maintainer's private alias map. The sanitization audience list is positive (external / client / regulator / SOC / procurement / internal-compliance / sales-engineering / partner draft / forwardable-internal); sanitize before any document leaves the implementation team's approved audience.
10
10
  >
@@ -6,7 +6,7 @@ Scope split with the sibling `mq-consumer-architecture.md` (which covers consume
6
6
 
7
7
  > **Conforms to the parallel-stack references pattern.** This file follows the layout documented in `skill-extraction-workflow/references/parallel-stack-references-pattern.md`: mirrored stack-agnostic core (when-applies through operations checklist), stack-specific implementation patterns section, and the embedded `### Mirrored-section grep gate` at the end of the stack-glue. The sibling `python-service-architecture/references/event-driven-architecture.md` mirrors the same structure. Either this file or `multi-tenant-isolation.md` may be used as a template for new parallel-stack extractions; multi-tenant additionally demonstrates the `## Topic-extension backlog` H2 for topic-wider-than-loop cases.
8
8
 
9
- > **Sibling sync.** A parallel `python-service-architecture/references/event-driven-architecture.md` mirrors **all non-stack-specific sections** of this file (when-applies/not-applies, delivery semantics, event vs command vs query, idempotency, outbox, ordering, schema evolution, retry/DLQ/replay, backpressure, fanout, saga, end-to-end exactly-once, anti-patterns, operations checklist). Only the *Go-specific implementation patterns* section diverges by stack. Maintainers updating any mirrored section here must update the sibling in the same change to prevent drift.
9
+ > **Sibling sync.** A parallel `python-service-architecture/references/event-driven-architecture.md` mirrors **all non-stack-specific sections** of this file (when-applies/not-applies, delivery semantics, event vs command vs query, idempotency, outbox, ordering, schema evolution, retry/DLQ/replay, backpressure, fanout, saga, end-to-end exactly-once, anti-patterns, operations checklist). Only the *Go-specific implementation patterns* section diverges by stack. Maintainers updating any mirrored section here must update the sibling in the same change to prevent drift. Tree-specific routing references are written inline for both trees so the mirrored bytes stay identical; cross-file parity is machine-checked by `skill-extraction-workflow/scripts/check-parallel-stack-parity.sh` (wired into `check-ccl-skills.sh`), which diffs the mirrored regions byte-for-byte (no normalization) and blocks on any divergence.
10
10
 
11
11
  > **Sanitization boundary.** The named brokers (Kafka, Pulsar, RabbitMQ, NATS JetStream, Redis Streams) and libraries below are concrete examples for **internal** implementation guidance, scoped to the implementation team's approved audience. Before this file (or excerpts) is copied into any document leaving that audience — external / client-facing materials, customer-specific deliverables, regulator or auditor evidence packages, SOC / compliance reports, procurement responses, or partner architecture appendices — replace the named choices with generic categories (`the broker`, `a partitioned log`, `a confirm-mode AMQP queue`) unless the vendor selection is already approved for disclosure to that specific audience.
12
12
 
@@ -20,8 +20,8 @@ Apply when the service:
20
20
 
21
21
  Skip when the service:
22
22
  - only does in-process pub-sub or fire-and-forget logging,
23
- - uses synchronous RPC with no durable async boundary (use `protobuf-contract-architecture.md` instead),
24
- - uses a job queue purely for in-tenant background work where loss is acceptable (use `notification-architecture.md` or `bulk-workflow-architecture.md`).
23
+ - uses synchronous HTTP/RPC with no durable async boundary (use `api-contract-and-schema.md` on the Python tree / `protobuf-contract-architecture.md` on the Go tree instead),
24
+ - uses a job queue purely for in-tenant background work where loss is acceptable (use `background-jobs-and-scheduling.md` or `batch-and-pipeline-architecture.md` on the Python tree / `notification-architecture.md` or `bulk-workflow-architecture.md` on the Go tree).
25
25
 
26
26
  ## Delivery semantics taxonomy
27
27
 
@@ -39,7 +39,7 @@ The semantic shape determines ownership, schema, and retry posture:
39
39
 
40
40
  - **Event** — a fact about something that happened. Past tense. Owned by the producer. Many consumers may subscribe. Schema evolves with backward-compatible additions; semantic meaning is fixed once published.
41
41
  - **Command** — a request to do something. Imperative. Owned by the recipient's contract. Typically one consumer (a command handler). May fail validation and be rejected; the sender is told.
42
- - **Query** — a request for state. Synchronous RPC or query API, not a durable message. If you find yourself sending a query as a message, you probably want a query API plus an event subscription for change notifications.
42
+ - **Query** — a request for state. Synchronous HTTP/RPC or query API, not a durable message. If you find yourself sending a query as a message, you probably want a query API plus an event subscription for change notifications.
43
43
 
44
44
  Mixing these confuses ownership: a command consumer that drops the message because "it's an event, consumers are best-effort" is a bug; an event producer that retries indefinitely because "it's a command, must deliver" creates head-of-line blocking.
45
45
 
@@ -77,7 +77,7 @@ The outbox pattern makes "write to DB and publish event" atomic without distribu
77
77
 
78
78
  Without one of these (and its prerequisites), `SKIP LOCKED` and per-key ordering are mutually exclusive — pick which property the outbox actually delivers and document it on the event contract.
79
79
 
80
- Inbox (the dual) is rarer in Go services: when a consumer must read a message and produce a side effect in a different system atomically, the consumer writes the inbox record + side effect in one DB tx, then acks the broker. On crash before ack, redelivery hits the inbox row and skips the side effect.
80
+ Inbox (the dual) is rarer: when a consumer must read a message and produce a side effect in a different system atomically, the consumer writes the inbox record + side effect in one DB tx, then acks the broker. On crash before ack, redelivery hits the inbox row and skips the side effect.
81
81
 
82
82
  ## Delivery ordering
83
83
 
@@ -100,13 +100,13 @@ Events outlive the producer's current code. Schema discipline is non-negotiable:
100
100
  - **Security / compliance exception** — when continuing to emit a field is itself the problem (leaky PII field, secret accidentally embedded, regulator-mandated retraction), additive-only is overruled. The path is: create a new event version that omits the field, inventory active consumers, deploy a coordinated emergency migration plan (consumer flips first, producer flips second, old version retired), and define historical-data handling (purge, redaction in archives, controlled access). Document the security trigger; this path is not the default deprecation flow.
101
101
  - **Rolling deprecation (the default)** — create a new event type or version, dual-publish or dual-read across a compatibility window, migrate consumers with monitoring and a rollback path, then retire the old version after explicit approval. Do not stop-the-world.
102
102
  - **Schema discovery model** — choose by audience:
103
- - *Single-team, single-broker boundary*: in-payload schema or schema embedded in the protobuf/Avro descriptor is acceptable.
103
+ - *Single-team, single-broker boundary*: in-payload schema or a model-derived schema (from the service's typed model layer) in the envelope header is acceptable.
104
104
  - *Multi-team, single-broker fanout*: a schema registry (Confluent-style or self-hosted) validates compatibility at build/deploy time.
105
105
  - *Multi-broker, multi-tenant, or external consumers* (webhooks, partner integrations, tenant-private contracts): hybrid — versioned envelope (`event_type`, `event_version`) in payload + per-tenant contract catalog/registry where available + signed schema references for external consumers + broker-specific validation gates. Neither a single registry nor in-payload alone is sufficient.
106
106
  - **Metadata leakage in cross-trust boundaries** — for external or multi-tenant contracts, the schema discovery layer itself can leak. `event_type` strings, version names, registry paths, enum labels, and catalog visibility can reveal unreleased products, regulated workflows, internal team structure, or tenant-specific capabilities even when payload fields are field-level protected. Before publishing to an external boundary: classify each of (`event_type`, `event_version`, schema-reference path, registry namespace, enum values, error codes, catalog listings) by audience; use opaque public aliases (`event_type=ext.<opaque-name>`, namespace-by-tenant-without-tenant-name) where the internal name is sensitive; never let an internal event type cross the boundary as-is.
107
107
  - **Breaking change discipline** — a semantic-level breaking change (a field's meaning changes, an enum value is repurposed) is not caught by schema compatibility checks. Document semantic changes; coordinate consumer rollout before publishing the new shape.
108
108
 
109
- For Go services using protobuf for events, see also `protobuf-contract-architecture.md` for `message`/`enum`/`oneof` evolution rules.
109
+ For event payloads modelled with the service's typed-model layer, freeze the model at publish time and version the envelope; the producer's evolving model must not reach the wire without a registered new version. The stack-glue section names the specific model library and freezing pattern.
110
110
 
111
111
  ## Retry, dead-letter, replay
112
112
 
@@ -135,11 +135,11 @@ Document which fixture class is used and which classes are deliberately not cove
135
135
  Consumer lag, broker queue depth, and producer rate are the three backpressure signals. Wire them:
136
136
 
137
137
  - **Consumer-side** — bound the work-in-flight, but **the shape depends on ordering**:
138
- - *Unordered consumer*: a bounded work-queue sized to the worker pool, N workers reading from the queue; shutdown via the request/service cancellation signal (the stack-glue section names the specific primitive). Order of completion is undefined.
139
- - *Ordered partitioned log* (Kafka, Pulsar key-shared, NATS JetStream ordered consumer): **one serial work lane per assigned partition/key**. Feed each partition into its own bounded channel + single worker, or process messages serially within the partition's poll loop. Feeding multiple ordered partitions into a single shared bounded channel + worker pool loses per-partition ordering, and a slow message on one partition can starve cold partitions or let later offsets overtake earlier ones. The bounded-queue-plus-pool shape is correct for unordered work queues; not for ordered partitioned logs.
138
+ - *Unordered consumer*: a bounded work-queue sized to the worker pool, N workers reading from the queue. A shared shutdown signal stops the workers (the stack-glue section names the specific primitive). Order of completion is undefined.
139
+ - *Ordered partitioned log* (Kafka, Pulsar key-shared, NATS JetStream ordered consumer): **one serial work lane per assigned partition/key**. Feed each partition into its own bounded work-queue + single worker, or process messages serially within the partition's poll loop. Feeding multiple ordered partitions into a single shared work-queue + worker pool loses per-partition ordering, and a slow message on one partition can starve cold partitions or let later offsets overtake earlier ones. The bounded-queue-plus-pool shape is correct for unordered work queues; not for ordered partitioned logs.
140
140
  - *Rebalance handling for partition-assigned consumers* — when a Kafka / Pulsar key-shared / similar consumer group rebalances and a partition is revoked, "one lane per partition" is unsafe without explicit rebalance discipline. The revoked owner must (1) stop fetching from the partition immediately, (2) drain or cancel its in-flight lane (await handler completion to a bounded deadline, or cancel with an explicit `partial-failure` disposition), (3) commit or abort offsets according to the handler outcome (commit only completed offsets; do not commit `last poll` blindly), and (4) be fenced so it cannot still publish a side effect after the new owner has started — typically by tagging each in-flight message with the assignment epoch and refusing side effects whose epoch is stale. Without fencing, the new owner and the old owner can process the same business key concurrently; per-partition ordering at steady state is meaningless if the rebalance window allows concurrent processing.
141
141
  - Avoid unbounded worker-per-message fanout in all cases (the stack-glue section names the specific anti-pattern API).
142
- - **Producer-side** — when the broker buffer fills (Kafka producer queue, NATS slow-consumer warning), block the producer's caller with a bounded wait or shed load at the producer entry point. Never block forever; surface a typed error after a bounded wait so upstream can backpressure further.
142
+ - **Producer-side** — when the broker buffer fills (Kafka producer queue, RabbitMQ unconfirmed-publishes limit, NATS slow-consumer warning), block the producer's caller with a bounded wait or shed load at the producer entry point. Never block forever; surface a typed error after a bounded wait so upstream can backpressure further.
143
143
  - **Cross-service** — a slow consumer is an upstream producer's problem to know about. Consumer lag must be exposed as a metric and alerted; producers cannot fix what they cannot see.
144
144
 
145
145
  ## Fanout patterns
@@ -187,6 +187,8 @@ Stack-agnostic recipe; document each clause for every event-driven boundary that
187
187
 
188
188
  If any clause is missing, the boundary is at-least-once with duplicates. Tell consumers honestly.
189
189
 
190
+ External grounding (adopted in part): this recipe is an instance of the end-to-end argument — [Saltzer, Reed & Clark, *End-to-End Arguments in System Design*, ACM TOCS 2(4), 1984](https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf) — a function that "can completely and correctly be implemented only with the knowledge and help of the application standing at the endpoints of the communication system" cannot be delegated to the communication layer, and broker-level transactional features are that paper's "incomplete version … useful as a performance enhancement", never the end-to-end guarantee. Borrowed scope: the placement argument only; the five-clause recipe and the atomic-domain boundary are this skill's own operational criteria.
191
+
190
192
  ## Anti-patterns
191
193
 
192
194
  - **Post-commit publish (durable cross-process)** — publishing the event after the DB transaction commits, without an outbox, when consumers are in another process. A crash between commit and publish silently drops the event. In-process, same-instance, rebuildable post-commit hooks are not this anti-pattern.
@@ -198,7 +200,7 @@ If any clause is missing, the boundary is at-least-once with duplicates. Tell co
198
200
  - **DLQ as graveyard** — messages land in DLQ, nobody looks, no replay tooling. The DLQ becomes a silent data-loss channel.
199
201
  - **Exactly-once claimed by broker badge** — broker config has an "exactly-once" mode, but the consumer is not idempotent and the producer is not transactional, OR the side effect is outside the atomic domain. The claim is wrong; record correct end-to-end semantics.
200
202
  - **Producer-held subscriber list as code** — subscriber list hardcoded at the producer when broker-side subscription is possible. (CDC bridges and webhook dispatchers are exceptions; their lists must live as config with audit and rotation.)
201
- - **Sync RPC as command** — a "command" sent via blocking RPC with retry and no replay path. If the call needs the durability of a queue, use a queue; if it needs the latency of RPC, accept best-effort.
203
+ - **Sync HTTP/RPC as command** — a "command" sent via blocking HTTP/RPC with retry and no replay path. If the call needs the durability of a queue, use a queue; if it needs the latency of HTTP/RPC, accept best-effort.
202
204
 
203
205
  ## Operations checklist (event-driven boundary launch)
204
206
 
@@ -231,6 +233,7 @@ These are stack-localized recipes that implement the stack-agnostic patterns abo
231
233
  3. **Mark-sent tx (short)**: `BEGIN; UPDATE outbox SET sent_at = NOW(), processing_until = NULL WHERE id = $id AND owner = $owner; COMMIT;` — guarded by `owner` so a re-leased row (after the original lease expired) is not double-marked.
232
234
  4. **Abandon tx**: after K publish failures, mark row `abandoned` and alert; do not block the partition key forever on a poison row (see the *Dispatcher that refuses to publish `id=N+1`* strategy).
233
235
  For per-key ordering across HA pollers, layer one of the strategies in *SKIP LOCKED and per-key ordering* (hash-routed publisher by `hash(partition_key) MOD N`, per-key advisory lease via the DB's advisory-lock primitive — PostgreSQL `pg_try_advisory_xact_lock(hashtext(partition_key))` for tx-scoped locks bound to the connection's current transaction; MySQL `GET_LOCK(name, timeout)` with explicit session-scoped semantics (release explicitly on success or stall, or rely on session close), and lock names server-wide-scoped so use a fully-bounded namespaced name `lk:<env8>:<svc8>:<purpose8>:<hash16>` (total length = 46 chars including separators, fits inside MySQL's 64-char limit; `<env8>`, `<svc8>`, `<purpose8>` are generated from the canonical environment / service / purpose identities by a **documented deterministic function** (e.g., first-8-of-base32(SHA256(canonical_identity))) or allocated from a **collision-checked registry** — human-readable abbreviations are NOT acceptable unless the registry proves uniqueness in the MySQL server-wide lock namespace; `<hash16>` is the first 16 chars of base32(SHA256(length-prefixed-encoding(canonical_partition_key, versioned_namespace))) — e.g., `SHA256(len(pk) + ':' + pk + len(ns) + ':' + ns)` or canonical JSON/CBOR over `[canonical_partition_key, versioned_namespace]`; raw `pk + ':' + ns` concatenation is **not** acceptable because real partition keys (`acme:prod`, `order:123`, user-supplied ids) can contain `:` and the resulting hash input is not injective. **Namespace-migration safety**: changing `versioned_namespace` requires either a drain / stop-the-world for the poller lane, or a dual-lock period (acquire old + new lock names in canonical order) so that mixed namespace versions across a rolling deploy / rollback cannot acquire different locks for the same partition key and publish concurrently. Without this, a version bump silently splits the per-key serialization lane.), and avoid MySQL NDB / multi-mysqld setups where `GET_LOCK` is not cluster-wide; TiDB supports MySQL-style user-level locks (`GET_LOCK`) cluster-wide in supported versions — verify timeout / deadlock semantics for the deployed TiDB version against a scenario-specific compatibility source: **pinned cluster** → checked-in version pin; **managed channel (TiDB Cloud)** → provider channel/SLA *and* current cluster version (channel alone is insufficient — the version still varies inside the channel); **rolling-upgrade fleet** → min/max active versions across the fleet plus the documented rolling-upgrade policy; **ad-hoc verification** → recorded `tidb_version()` output that includes cluster identity and timestamp. Otherwise fall back to an external coordinator (etcd lease, Redis `SET NX` with TTL) with fencing — with bounded TTL and a fencing token written with each publish; or refuse-newer-id dispatcher with abandoned-row state).
236
+ - **Event payload freezing** — the typed-model layer for Go event payloads is protobuf: freezing means serializing the payload to immutable bytes (marshal, or deep-copy then marshal) at the enqueue/publish boundary and binding those bytes to the envelope (`event_type`, `event_version`) — pinning the generated artifact version fixes the schema, not the instance: a queued mutable message object mutated before serialization changes the wire payload despite the pinned schema. See `protobuf-contract-architecture.md` for `message`/`enum`/`oneof` evolution rules.
234
237
  - **Idempotency storage** — pick by impact (see *Idempotency design*):
235
238
  - Lossy/rebuildable: Redis `SET dedup:<key> 1 EX <window> NX`, branch on success/skip.
236
239
  - Source-of-truth: insert into a `processed_events` table with `event_id` as primary key inside the side-effect transaction; on duplicate-key error, skip. Optionally cache the recent N keys in Redis as a hot-path filter, but Redis is not the authority.