@ccoalm/ccl-skills 0.7.0 → 0.9.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 (127) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +8 -7
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +6 -1
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/manual-invocation-and-prompts.md +6 -0
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +1 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/SKILL.md +3 -1
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +1 -1
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +1 -1
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +4 -1
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/state-machine-task-patterns.md +2 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/inference-capacity-operations.md +24 -0
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/llm-client-gateway.md +1 -1
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/model-prompt-evaluation.md +4 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +11 -10
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/contracts-and-state.md +5 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +73 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +3 -2
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/metrics-conventions.md +8 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +2 -2
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/canary-and-rollout-strategy.md +16 -2
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +9 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +14 -16
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/code-review-checklist.md +4 -0
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/rd-standards-doc-family-checklist.md +1 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +6 -5
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +3 -3
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +8 -1
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +16 -5
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +1 -1
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +4 -1
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +2 -0
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +1 -1
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +8 -8
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +4 -0
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +104 -5
  78. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
  79. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
  80. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +103 -0
  81. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +20 -0
  82. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
  83. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
  84. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +69 -2
  85. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
  86. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
  87. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
  88. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
  89. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
  90. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +17 -0
  91. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
  92. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
  93. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
  94. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
  95. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
  96. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
  97. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
  98. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
  99. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
  100. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
  101. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
  102. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
  103. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
  104. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
  105. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
  106. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
  107. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +8 -6
  108. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/classical-test-design-techniques.md +1 -1
  109. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-review-and-prioritization.md +1 -1
  110. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/update-lifecycle.md +2 -0
  111. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +16 -15
  112. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/ci-fixtures-and-flake-control.md +5 -1
  113. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
  114. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/e2e-real-flow-testing.md +2 -2
  115. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/integration-contract-testing.md +10 -0
  116. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +2 -2
  117. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-topology-and-commands.md +1 -1
  118. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +2 -1
  119. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/annotation-driven-revision.md +9 -0
  120. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +8 -2
  121. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +7 -5
  122. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
  123. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/react-architecture.md +3 -0
  124. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-quality-release.md +37 -4
  125. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-ui-quality.md +10 -1
  126. package/dist/assets/release.json +215 -105
  127. 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())
@@ -65,6 +65,7 @@ Use this skill for the full defect discipline: diagnose the immediate failure, f
65
65
 
66
66
  5. Verify cause.
67
67
  - Prove the cause with evidence.
68
+ - Report query/lookup evidence by cardinality: a data query, log search, or identity resolution that returns 0, 1, or N matches reports each of those outcomes distinctly — never silently take the first row of N, and never treat 0 rows as "no evidence collected" (an empty result over a named scope IS evidence: record which scopes matched and which were empty).
68
69
  - When the cause is environment/toolchain state, prove it from the tool that owns that state, not only from the high-level wrapper. A wrapper failure is a symptom until the underlying compiler, generator, runtime registry, dependency resolver, or platform destination evidence explains it.
69
70
  - If disproven, return to hypotheses instead of guessing.
70
71
  - Separate symptom, immediate cause, contributing factors, and prevention.
@@ -7,7 +7,9 @@ description: 风险定级 / 要不要灰度 / 需要哪些 gate / 双人 review
7
7
 
8
8
  Use this lightweight router before or during product delivery when the required rigor is unclear. It classifies risk and names the gates that must run; it does not replace `product-rd-workflow`, `testing-strategy`, `product-ui-ux-design`, stack-specific development skills, review skills, or release workflows.
9
9
 
10
- **Objective high-risk change-shapes force classification even when the change feels trivial — a low-risk intuition is not a substitute for running the tags.** The "when rigor is unclear" entry is not a licence to skip the router because you judged a change low-risk: the tags below classify well *once you are here*; the failure mode is never arriving because you felt sure. So regardless of that intuition, if the change deletes / backfills / migrates data or is otherwise irreversible, touches authentication / authorization / roles / tenant isolation, handles secrets / credentials / tokens, moves money or billing / quota, alters an external-facing or service-client API contract, changes a trust boundary or an untrusted-input sink, drives a production rollout / flag, or changes a shared deterministic gate / validator / CI harness / merge-readiness or completion-status rule (which can look docs/scripts-only yet decide future merge and completion semantics), run the classification FIRST and only then conclude low-risk if the relevant tags (`write-finality`, `data-migration`, `permission-access`, `security-review`, `api-contract`, `money-quota`, `release-ops`, `shared-gate`, `ai-action`) actually clear. Self-de-escalating one of those change-shapes to "not needed" without running its tag is the miss this prevents. (The always-on routing layer and `product-rd-workflow` owner-dispatch are the backstops for the case where the router is never invoked at all — this clause governs the case where you are deciding whether it applies.)
10
+ **Objective high-risk change-shapes force classification even when the change feels trivial — a low-risk intuition is not a substitute for running the tags.** The "when rigor is unclear" entry is not a licence to skip the router because you judged a change low-risk: the tags below classify well *once you are here*; the failure mode is never arriving because you felt sure. So regardless of that intuition, if the change deletes / backfills / migrates data or is otherwise irreversible, touches authentication / authorization / roles / tenant isolation, handles secrets / credentials / tokens, moves money or billing / quota, alters an external-facing or service-client API contract, changes a trust boundary or an untrusted-input sink, drives a production rollout / flag, or changes a shared deterministic gate / validator / CI harness / merge-readiness or completion-status rule (which can look docs/scripts-only yet decide future merge and completion semantics), run the classification FIRST and only then conclude low-risk if the relevant tags (`write-finality`, `data-migration`, `permission-access`, `security-review`, `api-contract`, `money-quota`, `release-ops`, `shared-gate`, `ai-action`) actually clear. Self-de-escalating one of those change-shapes to "not needed" without running its tag is the miss this prevents.
11
+
12
+ - Tone and diff size are not classification inputs: a reporter's urgent tone or executive pressure must never escalate a change's tags, and a small diff must never de-escalate them — tags run on the change's objective shape (production impact, frequency, money/data finality, workaround availability, rollback difficulty, verification sufficiency). (The always-on routing layer and `product-rd-workflow` owner-dispatch are the backstops for the case where the router is never invoked at all — this clause governs the case where you are deciding whether it applies.)
11
13
 
12
14
  ## Output Contract
13
15
 
@@ -89,7 +89,7 @@ Not appropriate for:
89
89
  - Keep transport DTOs, application parameters/results, domain objects, and storage models as separate boundaries when behavior or compatibility is non-trivial.
90
90
  - Patch/update contracts need presence semantics, not only zero values.
91
91
  - For finite values used across contracts, domain logic, persistence, clients, analytics, or events, architecture owns the semantic source of truth. Decide the canonical owner, shared contract/package location, allowed representations per boundary, parser/canonicalization owner, unknown/default behavior, and rollout order. If a shared location does not yet exist, the architecture decision must approve a local fallback and a consolidation task with owner and deadline; unowned `finite-value-debt` markers are architecture findings.
92
- - **RPC framework choice: Kitex remains the default; ConnectRPC is the credible 2025-2026 alternative for specific contexts**. Per `connectrpc.com` docs, Connect ships as a single small Go package (single-digit-thousand LOC), built on `net/http` with handlers implementing `http.Handler` and clients wrapping `http.Client` — works with any third-party router, middleware, or server. Supports three protocols (gRPC, gRPC-Web, Connect's own protocol) over both HTTP/1.1 and HTTP/2; any gRPC client in any language can call a Connect server, and Connect clients can call any gRPC server (validated by Google's interop tests). CNCF-incubated as of 2025; production-adopted at CrowdStrike, PlanetScale, Bluesky, Dropbox per `buf.build/blog`. Architecture choice: **choose Connect** when (a) the service hosts a browser-facing API and gRPC-Web is the main use case (Connect collapses gRPC + gRPC-Web + Connect into one server), (b) the service is small and Kitex's framework surface is over-spec'd, (c) the service must interop bidirectionally with multi-language gRPC clients but the team wants to avoid grpc-go's 130k-LOC dependency footprint. **Stay on Kitex** when (a) the team already operates Kitex across many services and the platform observability/middleware/registry contracts are Kitex-native, (b) Thrift IDL (TTHeader / TTHeader Streaming) is in active use alongside protobuf — Connect is protobuf-only, (c) Kitex-specific features (StreamX, FastCodec, generic call) are load-bearing. The two are NOT mutually exclusive: a Connect-based public-edge gateway can fan out to internal Kitex services, but **wire compatibility alone is not interop** — Connect speaks gRPC on the wire when configured for gRPC, and Kitex servers configured with the gRPC meta handler can accept those calls; however, Kitex services in production typically depend on Kitex-specific framework metadata (lane / tenant / caller-identity ctx values propagated via TTHeader or Kitex middleware), and Connect clients do not emit those by default. The fan-out works ONLY when (a) the target Kitex services are configured for the gRPC meta handler (not TTHeader), (b) the Connect-side Go client explicitly attaches the gRPC metadata (`metadata.MD`) that the Kitex service's middleware reads as caller-identity / lane / tenant — typically through a small adapter layer at the Connect-side that pulls fields from the Connect call context and writes them as gRPC headers, (c) any Kitex-specific TTHeader Streaming / generic call / Thrift-only paths stay routed through a Kitex-native edge instead. Plan the adapter layer as a first-class architecture component, not a one-line bridge; otherwise tenant / lane context drops silently at the protocol boundary and downstream services run without isolation.
92
+ - **RPC framework choice: Kitex remains the default; ConnectRPC is the credible 2025-2026 alternative for specific contexts**. Per `connectrpc.com` docs, Connect ships as a single small Go package (single-digit-thousand LOC), built on `net/http` with handlers implementing `http.Handler` and clients wrapping `http.Client` — works with any third-party router, middleware, or server. Supports three protocols (gRPC, gRPC-Web, Connect's own protocol) over both HTTP/1.1 and HTTP/2; any gRPC client in any language can call a Connect server, and Connect clients can call any gRPC server (the project validates gRPC compatibility with an extended version of Google's own gRPC interoperability test suite). CNCF **sandbox** project (per connectrpc.com, 2026-08 — not incubating/graduated); production-adopted at CrowdStrike, PlanetScale, Bluesky, Dropbox per `buf.build/blog`. Architecture choice: **choose Connect** when (a) the service hosts a browser-facing API and gRPC-Web is the main use case (Connect collapses gRPC + gRPC-Web + Connect into one server), (b) the service is small and Kitex's framework surface is over-spec'd, (c) the service must interop bidirectionally with multi-language gRPC clients but the team wants to avoid grpc-go's 130k-LOC dependency footprint. **Stay on Kitex** when (a) the team already operates Kitex across many services and the platform observability/middleware/registry contracts are Kitex-native, (b) Thrift IDL (TTHeader / TTHeader Streaming) is in active use alongside protobuf — Connect is protobuf-only, (c) Kitex-specific features (StreamX, FastCodec, generic call) are load-bearing. The two are NOT mutually exclusive: a Connect-based public-edge gateway can fan out to internal Kitex services, but **wire compatibility alone is not interop** — Connect speaks gRPC on the wire when configured for gRPC, and Kitex servers configured with the gRPC meta handler can accept those calls; however, Kitex services in production typically depend on Kitex-specific framework metadata (lane / tenant / caller-identity ctx values propagated via TTHeader or Kitex middleware), and Connect clients do not emit those by default. The fan-out works ONLY when (a) the target Kitex services are configured for the gRPC meta handler (not TTHeader), (b) the Connect-side Go client explicitly attaches the gRPC metadata (`metadata.MD`) that the Kitex service's middleware reads as caller-identity / lane / tenant — typically through a small adapter layer at the Connect-side that pulls fields from the Connect call context and writes them as gRPC headers, (c) any Kitex-specific TTHeader Streaming / generic call / Thrift-only paths stay routed through a Kitex-native edge instead. Plan the adapter layer as a first-class architecture component, not a one-line bridge; otherwise tenant / lane context drops silently at the protocol boundary and downstream services run without isolation.
93
93
 
94
94
  ## Data Design
95
95
 
@@ -162,7 +162,7 @@ Tenant commitments around where data lives and who can see it shape the isolatio
162
162
  - **Residency** — "tenant X's data stays in region Y" is a region-per-tenant or region-pinned commitment; the data plane (DB, object storage, backup, analytics) all honor it; the routing layer enforces it.
163
163
  - **Sovereignty** — government / regulated tenants may require a separate stack with no cross-border access; this is a deployment-level isolation, not a runtime knob.
164
164
  - **Encryption** — at-rest encryption per tenant (separate keys per tenant) is a stronger model than a shared key.
165
- - **Crypto-deletion is conditional, not universal** — destroying the per-tenant key is acceptable proof of erasure **only when** the key hierarchy, key backups, envelope keys, restore paths, and the relevant regulator's interpretation all support it. NIST SP 800-88 treats cryptographic erase as a sanitization technique with conditions; some interpretations of GDPR distinguish anonymization (irreversible) from pseudonymization (key-linkable encrypted data may still be personal data). Where any condition fails, crypto-deletion is a *beyond-use / suppression* control (one of the per-store deletion modes above), not proof of erasure; the deletion workflow records the actual mode achieved per tenant per store, and the tenant is told what was achieved. **Required evidence before recording "erasure" via crypto-deletion**, per store and per tenant; each artifact must be authentic to *this* deletion, not theatrical paperwork:
165
+ - **Crypto-deletion is conditional, not universal** — destroying the per-tenant key is acceptable proof of erasure **only when** the key hierarchy, key backups, envelope keys, restore paths, and the relevant regulator's interpretation all support it. NIST SP 800-88 treats cryptographic erase as a sanitization technique with explicit conditions — do not use CE when data predates encryption enablement or when keys were backed up/escrowed without verified protection (conditions as stated in Rev.1 §2.6; Rev.2, 2025, supersedes Rev.1 and continues the CE-conditions framework — verify the corresponding Rev.2 section when citing it as the authority); and EDPB guidance (e.g. Guidelines 02/2025) holds that encrypted personal data remains personal data "at least until the algorithm is broken", so key destruction is a conditional control, not automatic GDPR erasure. Where any condition fails, crypto-deletion is a *beyond-use / suppression* control (one of the per-store deletion modes above), not proof of erasure; the deletion workflow records the actual mode achieved per tenant per store, and the tenant is told what was achieved. **Required evidence before recording "erasure" via crypto-deletion**, per store and per tenant; each artifact must be authentic to *this* deletion, not theatrical paperwork:
166
166
  - *(a) key hierarchy diagram* — scoped to this store and this tenant's key version, dated within a defined freshness window (e.g., last 30 days), showing every key that wraps or could reconstruct the data.
167
167
  - *(b) key-backup inventory* — for this store, scoped to this tenant's keys, naming every backup location, rotation policy, and the holder of each backup; dated within the freshness window.
168
168
  - *(c) restore-path test result* — run against *this* backup store with *this* tenant's key-version destroyed, confirming the restore fails because the key is gone. A restore test on a different store or a different key-version is not evidence.
@@ -18,7 +18,10 @@ Use this for implementation of new backend products and services. It should adap
18
18
  - Use codebase-specific skills only when the task is explicitly about an existing legacy/workspace repository.
19
19
  - Development references should turn already-chosen architecture into code, tests, and generated artifacts. If a change requires choosing ownership, security model, source of truth, or release governance, first apply `go-microservice-architecture`.
20
20
  - For money, billing, quota, permission, tenant/user data isolation, high-impact AI, repeated writes, async finality, or incident-explanation risk, apply `product-rd-workflow` high-risk resilience gates and route test-layer design through `testing-strategy`.
21
- - When a change edits strings, templates, or config values that are returned to, persisted for, emitted to, served to, synchronized with, or configured for client consumption (error copy, labels, notification text, localization payloads, content/CMS/seed rows, message or notification templates, flag-delivered content), classify the consumers with a recorded bounded check (client repo / contract / locale search) before closing on API/log evidence; if any client surface renders the value user-facing, or consumers are unknown, load `product-ui-ux-design` and record its implementation-owner checkpoint — including the consuming client stack owner(s) and `testing-strategy` per that checkpoint's field list — with client-side rendered-evidence routing. Backend-only closure without that recorded consumer check is invalid.
21
+ - When a change can alter what a client renders or which state, action, or decision path it offers—including strings/templates/config/flags and API/event/schema fields, enums, status/progress, permission/capability signals, defaults, or result shapes—load `../product-ui-ux-design/references/delivery-contract.md`, create the applicable full or lightweight record in that contract, and follow its canonical consumer-universe classification, design/test/client handoffs, and terminal-status rules.
22
+ - This Go owner returns only its `producer_record` delta: immutable binding, build/schema/config artifact identity, exact command/environment, and API/event/log/output observation.
23
+
24
+ - For a standalone Go CLI, this skill owns Go parser/library implementation mechanics. Any change to a user-facing command tree, subcommand, flag/default/action path, help/output/exit behavior, confirmation, progress, or recovery path also loads `terminal-cli-dev`, which owns the terminal contract and its UI/UX/testing handoff. Only internal parser refactors proven to preserve all user-visible semantics may skip that owner.
22
25
 
23
26
  ## Generalization Discipline
24
27
 
@@ -22,6 +22,8 @@ For generic async, consumer, scheduled-job, panic recovery, context timeout, and
22
22
  - Start transitions should move pending work to processing before expensive work.
23
23
  - Failure transitions should capture canonical error code, safe message, retryable flag, retry count, and last trace or log id.
24
24
  - Success transitions should persist result pointer or summary before publishing completion events.
25
+ - All timestamps the transition itself stamps come from a single captured `now` (capturing the clock twice inside one transition produces `finished_at < started_at` records or audit rows disagreeing with the state row under load); domain-provided times — an upstream completion time, an event time — are recorded as received, never re-stamped with the local `now`.
26
+ - Validate external/dependency response structure before casting or mapping it: an assertion/parse step with a typed error path, never a blind cast — a malformed upstream payload must become a failure transition with the canonical error, not a panic or silently-zeroed field.
25
27
 
26
28
  ## Async Processing
27
29
 
@@ -16,7 +16,8 @@ Use this for product backend work that calls, hosts, evaluates, or operates LLM
16
16
  - Use `product-rd-workflow` first when the request spans product goal, PRD, architecture, implementation plan, release, and learning loop.
17
17
  - Use `product-rd-workflow` first for AI/algorithm product launch SOPs, business acceptance baselines, build-vs-buy ROI, new-vs-iteration launch gates, or multi-algorithm product quality gates. This skill owns inference implementation/evaluation mechanics after the product gate is defined.
18
18
  - For high-impact answers or decisions where wrong output can mislead users, affect money/rights/access, or create support/compliance risk, use `product-rd-workflow` high-risk resilience gates before fallback, downgrade, or launch decisions.
19
- - When a change edits strings, templates, or config values that are returned to, persisted for, emitted to, served to, synchronized with, or configured for client consumption (error copy, labels, notification text, localization payloads, content/CMS/seed rows, message or notification templates, flag-delivered content), classify the consumers with a recorded bounded check (client repo / contract / locale search) before closing on API/log evidence; if any client surface renders the value user-facing, or consumers are unknown, load `product-ui-ux-design` and record its implementation-owner checkpoint — including the consuming client stack owner(s) and `testing-strategy` per that checkpoint's field list — with client-side rendered-evidence routing. Inference-side closure without that recorded consumer check is invalid.
19
+ - When a change can alter what a client renders or which state, action, or decision path it offers—including strings/templates/config/flags and API/event/schema fields, enums, status/progress, permission/capability signals, defaults, or result shapes—you must load `../product-ui-ux-design/references/delivery-contract.md`, create the applicable full or lightweight record in that contract, and follow its canonical consumer-universe classification, design/test/client handoffs, and terminal-status rules.
20
+ - This inference owner returns only its `producer_record` delta: immutable binding, prompt/model/config/artifact identity, exact command/environment, and API/event/log/output observation.
20
21
 
21
22
  ## Generalization Discipline
22
23
 
@@ -43,6 +43,30 @@ Before rollout, run a bounded capacity check for:
43
43
 
44
44
  Use dry-run or report-only modes for migration/backfill/batch jobs whenever possible.
45
45
 
46
+ ## Provider Evaluation Evidence Ladder
47
+
48
+ When the decision is "adopt / switch to / gray-ramp provider X or model Y for a real workload" (procurement or migration, not routine regression), the two questions are: is it cheaper on the real workload, and is it stable at the required load — and neither is answerable from a model list, a public price page, or one successful request.
49
+
50
+ **Contract before the first paid call.** Write down: exact environment/model IDs/protocol/credential scope; the production request-shape distribution being simulated; the capability-parity checklist; **spend cap, request cap, wall-time cap, automatic stop conditions, and who approved the budget** — enforced by reservation at admission (mechanics in the bullet below), so the invariant is cap ≥ reconciled spend + outstanding reservations at all times; the stop latch halts new admissions, not merely accounting; and the acceptance thresholds plus which decision this run feeds. Thresholds precede data (measurement-design discipline); a run whose stop conditions were invented after the spend is not an evaluation. Before any run that costs money or creates external resources, show the (redacted) config plus the exact commands and estimated cost/duration and get explicit confirmation — never proceed on inferred consent.
51
+
52
+ - Reservation mechanics: admission must be an atomic check-and-reserve against a single reservation ledger — read-headroom-then-reserve is a TOCTOU that lets concurrent workers jointly overshoot the cap — and a reservation releases only on billing reconciliation, never on completion alone (usage/billing signals lag).
53
+
54
+ **Seven evidence layers — a stronger layer may use lower layers as context, never substitute for them:**
55
+ 1. published claim (price page / model card) →
56
+ 2. authenticated control-plane fact (the model is actually on this account/region) →
57
+ 3. single-call data-plane success →
58
+ 4. capability parity on the checklist (structured output, tool use, streaming, context length — against the exact model string; aliases/version suffixes/casing variants are non-equivalent until proven) →
59
+ 5. capacity/stability under sustained load →
60
+ 6. billing reconciliation (billed vs calculated within tolerance, e.g. ≤2%; failure/cancel/refund behavior checked against the contract) →
61
+ 7. end-to-end on the real workload path.
62
+ A PASS verdict binds to the **exact** account/model/region/protocol combination that produced layers 3-7; anything less is CONDITIONAL/FAIL/BLOCKED naming the missing layer.
63
+
64
+ **Load-methodology minima:** drive load open-loop at a fixed arrival rate — closed-loop concurrency hides throughput loss when requests slow down (coordinated-omission family: Schroeder et al., "Open Versus Closed", NSDI'06; Gil Tene's coordinated-omission analysis for the latency-distortion half). Report first-attempt results separately from retry-assisted results. Make warmup semantics explicit — including cold start measures the real user experience, excluding it measures steady-state capability; they are two different experiments, name which one you ran. Step the load (fractional → 1× → burst) with pre-set stop thresholds per stage, include a recovery segment (idle then back to 1×), and never continue to a higher stage merely to fill a report.
65
+
66
+ - A sample count below ~3 runs per cell must be reported as unreliable, never averaged into a verdict (team heuristic, no external source — raise the floor per your observed variance).
67
+
68
+ **Cost accounting:** separate the five cost frames — internal charge-back price, incumbent's actually-paid price, candidate's nominal price, candidate's measured price on the real workload, and cash cost (tax/discount/FX) — and state which frames are assumptions rather than quotes. Task submission ≠ success: success requires the expected terminal state, a usable artifact, normalized usage, and billing evidence; an HTTP 200 carrying an error envelope is a failure. The attempt ledger is append-only — a retry never overwrites a failed attempt's record.
69
+
46
70
  ## Fine-Tuning And Local Models
47
71
 
48
72
  - Treat fine-tuned models as registry versions with parent base model, training data lineage, training job id, parameter recipe, eval gate, and rollback path.