opencode-skills-collection 4.1.2 → 4.1.4
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.
- package/bundled-skills/.antigravity-install-manifest.json +31 -1
- package/bundled-skills/ai-agent-evaluation-benchmarking/SKILL.md +124 -0
- package/bundled-skills/ai-agent-tool-routing/SKILL.md +136 -0
- package/bundled-skills/ai-code-generation-guardrails/SKILL.md +142 -0
- package/bundled-skills/ai-prompt-regression-testing/SKILL.md +130 -0
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +26 -8
- package/bundled-skills/cline-pilot/README.md +138 -0
- package/bundled-skills/cline-pilot/SKILL.md +301 -0
- package/bundled-skills/cline-pilot/assets/global-memory-bank-prompt.en.md +66 -0
- package/bundled-skills/cline-pilot/assets/global-memory-bank-prompt.md +56 -0
- package/bundled-skills/cline-pilot/references/cold-start.md +58 -0
- package/bundled-skills/cline-pilot/references/decision-log.example.md +9 -0
- package/bundled-skills/cline-pilot/references/local-config.example.md +26 -0
- package/bundled-skills/cline-pilot/references/project-profiles.example.md +10 -0
- package/bundled-skills/cline-pilot/scripts/session_report.py +151 -0
- package/bundled-skills/cross-chain-relayer-audit/SKILL.md +136 -0
- package/bundled-skills/deep-research-framework/SKILL.md +81 -0
- package/bundled-skills/defi-yield-strategy-allocator/SKILL.md +124 -0
- package/bundled-skills/dropthehassle-publish/SKILL.md +151 -0
- package/bundled-skills/five-axis-code-review/SKILL.md +78 -0
- package/bundled-skills/git-commit-message/SKILL.md +76 -0
- package/bundled-skills/idea-to-blueprint/LICENSE +21 -0
- package/bundled-skills/idea-to-blueprint/SKILL.md +260 -0
- package/bundled-skills/idea-to-blueprint/assets/AGENTS.md.template +38 -0
- package/bundled-skills/idea-to-blueprint/assets/CLAUDE.md.template +38 -0
- package/bundled-skills/idea-to-blueprint/assets/DECISIONS.md.template +14 -0
- package/bundled-skills/idea-to-blueprint/assets/PROGRESS.md.template +47 -0
- package/bundled-skills/idea-to-blueprint/assets/session-prompt.md +23 -0
- package/bundled-skills/idea-to-blueprint/references/architecture-decisions.md +202 -0
- package/bundled-skills/idea-to-blueprint/references/blueprint-template.md +259 -0
- package/bundled-skills/idea-to-blueprint/references/code-quality-and-review.md +216 -0
- package/bundled-skills/idea-to-blueprint/references/design-minimal.md +96 -0
- package/bundled-skills/idea-to-blueprint/references/edge-case-catalog.md +239 -0
- package/bundled-skills/idea-to-blueprint/references/intake-questions.md +105 -0
- package/bundled-skills/idea-to-blueprint/references/research-protocol.md +196 -0
- package/bundled-skills/idea-to-blueprint/references/session-protocol.md +133 -0
- package/bundled-skills/idea-to-blueprint/references/stories-ac-tests.md +194 -0
- package/bundled-skills/idea-to-blueprint/references/ux-writing-guide.md +128 -0
- package/bundled-skills/idea-to-blueprint/scripts/lint_blueprint.py +460 -0
- package/bundled-skills/liuguang-banlan-ui/SKILL.md +22 -18
- package/bundled-skills/liuguang-banlan-ui/assets/starter/launcher.css +2 -2
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/index.html +5 -4
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme-config.js +3 -10
- package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme.css +48 -62
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/index.html +5 -4
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme-config.js +27 -35
- package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme.css +48 -62
- package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/spectral-field.js +135 -148
- package/bundled-skills/liuguang-banlan-ui/references/style-contract.md +26 -7
- package/bundled-skills/liuguang-banlan-ui/references/verification.md +19 -7
- package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +78 -47
- package/bundled-skills/liuguang-banlan-ui/scripts/serve_preview.py +35 -0
- package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +34 -3
- package/bundled-skills/liuguang-banlan-ui/tests/test_measure_preview.py +119 -0
- package/bundled-skills/liuguang-banlan-ui/tests/test_serve_preview.py +38 -0
- package/bundled-skills/liuguang-banlan-ui/tests/test_validate_manifest.py +73 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +12 -12
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +1 -1
- package/bundled-skills/meeting-notes/SKILL.md +80 -0
- package/bundled-skills/repo-foundation/SKILL.md +166 -0
- package/bundled-skills/repo-foundation/references/LICENSE.md +21 -0
- package/bundled-skills/repo-foundation/references/bootstrap.md +63 -0
- package/bundled-skills/repo-foundation/references/companion.md +9 -0
- package/bundled-skills/repo-foundation/references/continuity.md +50 -0
- package/bundled-skills/repo-foundation/references/evolution.md +60 -0
- package/bundled-skills/repo-foundation/references/migration-examples.md +53 -0
- package/bundled-skills/repo-foundation/references/shared-contracts.md +48 -0
- package/bundled-skills/repo-foundation/references/verification.md +50 -0
- package/bundled-skills/repo-native-refactor/SKILL.md +225 -0
- package/bundled-skills/repo-native-refactor/references/LICENSE.md +21 -0
- package/bundled-skills/repo-native-refactor/references/deterministic-tooling.md +101 -0
- package/bundled-skills/repo-native-refactor/references/error-reliability.md +117 -0
- package/bundled-skills/repo-native-refactor/references/finding-taxonomy.md +181 -0
- package/bundled-skills/repo-native-refactor/references/refactor-examples.md +77 -0
- package/bundled-skills/repo-native-refactor/references/repository-prose.md +157 -0
- package/bundled-skills/repo-native-refactor/references/repository-rehabilitation.md +94 -0
- package/bundled-skills/repo-native-refactor/references/semantic-risk.md +187 -0
- package/bundled-skills/repo-native-refactor/references/shared-contracts.md +48 -0
- package/bundled-skills/repo-native-refactor/references/testing-integrity.md +103 -0
- package/bundled-skills/smart-contract-formal-verification/SKILL.md +136 -0
- package/bundled-skills/smart-contract-upgrade-governance/SKILL.md +130 -0
- package/bundled-skills/soroban-contract-audit/SKILL.md +136 -0
- package/bundled-skills/soroban-liquidity-pool/SKILL.md +118 -0
- package/bundled-skills/soroban-oracle-data-feed-audit/SKILL.md +118 -0
- package/bundled-skills/soroban-storage-ttl-lifecycle/SKILL.md +118 -0
- package/bundled-skills/soroban-token-minter/SKILL.md +124 -0
- package/bundled-skills/stellar-anchor-integration/SKILL.md +136 -0
- package/bundled-skills/stellar-asset-clawback-compliance/SKILL.md +118 -0
- package/bundled-skills/stellar-escrow-timelock/SKILL.md +118 -0
- package/bundled-skills/stellar-multisig-threshold-coordinator/SKILL.md +112 -0
- package/bundled-skills/tech-writing-proofread/SKILL.md +78 -0
- package/bundled-skills/typescript-expert/scripts/ts_diagnostic.py +40 -19
- package/bundled-skills/waf-setup/SKILL.md +1 -1
- package/bundled-skills/web3-rate-limiting-circuit-breaker/SKILL.md +130 -0
- package/bundled-skills/web3-transaction-relayer-pool/SKILL.md +124 -0
- package/bundled-skills/youtube-summarizer/SKILL.md +3 -3
- package/bundled-skills/zk-proof-verification-pipeline/SKILL.md +124 -0
- package/package.json +1 -1
- package/skills_index.json +1059 -0
|
@@ -30,17 +30,26 @@ except ModuleNotFoundError as exc:
|
|
|
30
30
|
raise SystemExit(2)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
# A color counts as visible when it deviates this far from the base (OKLab).
|
|
34
|
+
TINT_THRESHOLD = {"opal": 0.003, "obsidian": 0.004}
|
|
35
|
+
# A block is gray when its lightness moves this far with little hue change.
|
|
36
|
+
GRAY_SHIFT = 0.003
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_config(path: pathlib.Path) -> dict:
|
|
40
|
+
return load_manifest(path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def srgb_to_linear(rgb: np.ndarray) -> np.ndarray:
|
|
44
|
+
return np.where(rgb <= 0.04045, rgb / 12.92, ((rgb + 0.055) / 1.055) ** 2.4)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def linear_to_oklab(linear: np.ndarray) -> np.ndarray:
|
|
39
48
|
r, g, b = np.moveaxis(linear, -1, 0)
|
|
40
49
|
l = np.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
|
|
41
50
|
m = np.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
|
|
42
51
|
s = np.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
|
|
43
|
-
|
|
52
|
+
return np.stack(
|
|
44
53
|
(
|
|
45
54
|
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
46
55
|
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
@@ -48,38 +57,50 @@ def srgb_to_oklab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
|
48
57
|
),
|
|
49
58
|
axis=-1,
|
|
50
59
|
)
|
|
51
|
-
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
52
|
-
return lab, luminance
|
|
53
60
|
|
|
54
61
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
return np.
|
|
62
|
+
def oklch_to_ab(color: dict) -> np.ndarray:
|
|
63
|
+
hue = np.radians(float(color["h"]))
|
|
64
|
+
return float(color["c"]) * np.array([np.cos(hue), np.sin(hue)])
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
def block_average(linear: np.ndarray, block: int) -> np.ndarray:
|
|
68
|
+
block = max(1, min(block, linear.shape[0], linear.shape[1]))
|
|
69
|
+
h = linear.shape[0] // block * block
|
|
70
|
+
w = linear.shape[1] // block * block
|
|
71
|
+
trimmed = linear[:h, :w].reshape(h // block, block, w // block, block, 3)
|
|
72
|
+
return trimmed.mean(axis=(1, 3))
|
|
66
73
|
|
|
67
|
-
base_chroma = float(config["base"]["oklch"]["c"])
|
|
68
|
-
threshold = base_chroma + (0.003 if config["mode"] == "opal" else 0.004)
|
|
69
|
-
chroma_energy = np.maximum(chroma - threshold, 0.0)
|
|
70
|
-
visible = chroma_energy > 0
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
def measure(image_path: pathlib.Path, config: dict, block: int = 8) -> dict:
|
|
76
|
+
with Image.open(image_path) as source:
|
|
77
|
+
image = source.convert("RGB")
|
|
78
|
+
linear = srgb_to_linear(np.asarray(image, dtype=np.float64) / 255.0)
|
|
79
|
+
luminance = 0.2126 * linear[..., 0] + 0.7152 * linear[..., 1] + 0.0722 * linear[..., 2]
|
|
80
|
+
lab = linear_to_oklab(block_average(linear, block))
|
|
81
|
+
|
|
82
|
+
base = config["base"]["oklch"]
|
|
83
|
+
base_ab = oklch_to_ab(base)
|
|
84
|
+
shift = lab[..., 0] - float(base["l"])
|
|
85
|
+
delta = lab[..., 1:] - base_ab
|
|
86
|
+
tint = np.hypot(delta[..., 0], delta[..., 1])
|
|
87
|
+
|
|
88
|
+
threshold = TINT_THRESHOLD.get(config["mode"], 0.003)
|
|
89
|
+
energy = np.maximum(tint - threshold, 0.0)
|
|
90
|
+
visible = energy > 0
|
|
91
|
+
gray = (np.abs(shift) > GRAY_SHIFT) & (tint < np.abs(shift) / 3)
|
|
92
|
+
|
|
93
|
+
directions = np.stack([oklch_to_ab(color["oklch"]) - base_ab for color in config["colors"]])
|
|
94
|
+
angles = np.arctan2(directions[:, 1], directions[:, 0])
|
|
95
|
+
deviation_angle = np.arctan2(delta[..., 1], delta[..., 0])
|
|
96
|
+
distance = np.abs((deviation_angle[..., None] - angles + np.pi) % (2 * np.pi) - np.pi)
|
|
97
|
+
assignments = np.argmin(distance, axis=-1)
|
|
98
|
+
|
|
99
|
+
total_blocks = tint.size
|
|
100
|
+
total_energy = float(energy.sum())
|
|
79
101
|
colors = []
|
|
80
102
|
for index, color in enumerate(config["colors"]):
|
|
81
103
|
assigned = visible & (assignments == index)
|
|
82
|
-
energy = float(chroma_energy[assigned].sum())
|
|
83
104
|
colors.append(
|
|
84
105
|
{
|
|
85
106
|
"id": color["id"],
|
|
@@ -87,24 +108,40 @@ def measure(image_path: pathlib.Path, config: dict) -> dict:
|
|
|
87
108
|
"configuredIntensity": color["intensity"],
|
|
88
109
|
"oklch": color["oklch"],
|
|
89
110
|
"peakOpacity": color["peakOpacity"],
|
|
90
|
-
"measuredCoverage": round(float(assigned.sum()) /
|
|
91
|
-
"effectiveShare": round(energy / total_energy, 4) if total_energy else 0.0,
|
|
111
|
+
"measuredCoverage": round(float(assigned.sum()) / total_blocks, 4),
|
|
112
|
+
"effectiveShare": round(float(energy[assigned].sum()) / total_energy, 4) if total_energy else 0.0,
|
|
92
113
|
}
|
|
93
114
|
)
|
|
94
115
|
|
|
116
|
+
mean_tint = float(tint.mean())
|
|
95
117
|
return {
|
|
96
118
|
"image": image_path.name,
|
|
97
119
|
"dimensions": {"width": image.width, "height": image.height},
|
|
98
|
-
"method":
|
|
99
|
-
|
|
120
|
+
"method": (
|
|
121
|
+
f"OKLab deviation from the configured base over {block}x{block} pixel blocks; "
|
|
122
|
+
"hue is attributed by the direction of the deviation"
|
|
123
|
+
),
|
|
124
|
+
"blockSize": block,
|
|
125
|
+
"visibilityThresholdTint": threshold,
|
|
100
126
|
"chromaticPixelRatio": round(float(visible.mean()), 4),
|
|
101
|
-
"
|
|
127
|
+
"tint": {
|
|
128
|
+
"mean": round(mean_tint, 5),
|
|
129
|
+
"p50": round(float(np.percentile(tint, 50)), 5),
|
|
130
|
+
"p95": round(float(np.percentile(tint, 95)), 5),
|
|
131
|
+
},
|
|
132
|
+
"lightnessShift": {
|
|
133
|
+
"mean": round(float(shift.mean()), 5),
|
|
134
|
+
"p05": round(float(np.percentile(shift, 5)), 5),
|
|
135
|
+
"p95": round(float(np.percentile(shift, 95)), 5),
|
|
136
|
+
},
|
|
137
|
+
"shiftToTint": round(abs(float(shift.mean())) / mean_tint, 3) if mean_tint else None,
|
|
138
|
+
"grayRatio": round(float(gray.mean()), 4) if config["mode"] == "opal" else None,
|
|
102
139
|
"luminance": {
|
|
103
140
|
"mean": round(float(luminance.mean()), 5),
|
|
104
141
|
"p05": round(float(np.percentile(luminance, 5)), 5),
|
|
105
142
|
"p95": round(float(np.percentile(luminance, 95)), 5),
|
|
106
143
|
"max": round(float(luminance.max()), 5),
|
|
107
|
-
"configuredCap": config["field"]
|
|
144
|
+
"configuredCap": config["field"].get("luminanceCap"),
|
|
108
145
|
},
|
|
109
146
|
"colors": colors,
|
|
110
147
|
}
|
|
@@ -114,25 +151,19 @@ def main() -> None:
|
|
|
114
151
|
parser = argparse.ArgumentParser()
|
|
115
152
|
parser.add_argument("--image", required=True, type=pathlib.Path)
|
|
116
153
|
parser.add_argument("--config", required=True, type=pathlib.Path)
|
|
154
|
+
parser.add_argument("--block", type=int, default=8, help="pixel block size used to average out dither")
|
|
117
155
|
args = parser.parse_args()
|
|
118
156
|
try:
|
|
119
|
-
config =
|
|
157
|
+
config = load_config(args.config.resolve())
|
|
120
158
|
except (ManifestSyntaxError, OSError) as error:
|
|
121
|
-
print(
|
|
122
|
-
json.dumps(
|
|
123
|
-
{"status": "invalid", "errors": [str(error)]},
|
|
124
|
-
ensure_ascii=False,
|
|
125
|
-
indent=2,
|
|
126
|
-
),
|
|
127
|
-
file=sys.stderr,
|
|
128
|
-
)
|
|
159
|
+
print(json.dumps({"status": "invalid", "errors": [str(error)]}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
129
160
|
raise SystemExit(1) from error
|
|
130
161
|
payload = {
|
|
131
|
-
"schemaVersion": "1.
|
|
162
|
+
"schemaVersion": "1.1",
|
|
132
163
|
"theme": config["label"],
|
|
133
164
|
"preset": config["preset"],
|
|
134
165
|
"overallColorIntensity": config["overallColorIntensity"],
|
|
135
|
-
"measurement": measure(args.image.resolve(), config),
|
|
166
|
+
"measurement": measure(args.image.resolve(), config, args.block),
|
|
136
167
|
}
|
|
137
168
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
138
169
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Serve a directory over local HTTP with caching disabled, for previews and screenshots."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import functools
|
|
6
|
+
import http.server
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# python -m http.server sends Last-Modified without Cache-Control, so browsers keep
|
|
11
|
+
# serving a stale theme-config.js after an edit and screenshots measure the old field.
|
|
12
|
+
class NoStoreHandler(http.server.SimpleHTTPRequestHandler):
|
|
13
|
+
def end_headers(self):
|
|
14
|
+
self.send_header("Cache-Control", "no-store")
|
|
15
|
+
super().end_headers()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
20
|
+
parser.add_argument("directory", nargs="?", type=pathlib.Path, default=pathlib.Path("."))
|
|
21
|
+
parser.add_argument("--port", type=int, default=8000)
|
|
22
|
+
parser.add_argument("--bind", default="127.0.0.1")
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
root = args.directory.resolve()
|
|
25
|
+
handler = functools.partial(NoStoreHandler, directory=str(root))
|
|
26
|
+
with http.server.ThreadingHTTPServer((args.bind, args.port), handler) as server:
|
|
27
|
+
print(f"serving {root} at http://{args.bind}:{args.port}/ with Cache-Control: no-store")
|
|
28
|
+
try:
|
|
29
|
+
server.serve_forever()
|
|
30
|
+
except KeyboardInterrupt:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -126,7 +126,7 @@ def validate(config: dict) -> list[str]:
|
|
|
126
126
|
if not isinstance(field, dict):
|
|
127
127
|
errors.append("field must be an object")
|
|
128
128
|
field = {}
|
|
129
|
-
for key in ("scale", "octaves", "warpStrength", "motionSpeed", "staticTime", "ditherStrength"
|
|
129
|
+
for key in ("scale", "octaves", "warpStrength", "motionSpeed", "staticTime", "ditherStrength"):
|
|
130
130
|
require(key in field, f"field missing {key}", errors)
|
|
131
131
|
check_range(field.get("scale"), 0, float("inf"), "field.scale must be positive", errors, lower_inclusive=False)
|
|
132
132
|
octaves = field.get("octaves")
|
|
@@ -139,10 +139,37 @@ def validate(config: dict) -> list[str]:
|
|
|
139
139
|
check_range(field.get("motionSpeed"), 0, float("inf"), "field.motionSpeed must be finite and nonnegative", errors)
|
|
140
140
|
check_finite(field.get("staticTime"), "field.staticTime must be a finite number", errors)
|
|
141
141
|
check_range(field.get("ditherStrength"), 0, 1, "field.ditherStrength must be in [0, 1]", errors)
|
|
142
|
-
|
|
142
|
+
if config["mode"] == "obsidian" or "luminanceCap" in field:
|
|
143
|
+
check_range(field.get("luminanceCap"), 0, 1, "field.luminanceCap must be in (0, 1]", errors, lower_inclusive=False)
|
|
143
144
|
return errors
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
def hue_travel(hues: list[float]) -> float:
|
|
148
|
+
steps = (abs(a - b) % 360 for a, b in zip(hues, hues[1:] + hues[:1]))
|
|
149
|
+
return sum(min(step, 360 - step) for step in steps)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def best_hue_travel(hues: list[float]) -> float:
|
|
153
|
+
ordered = sorted(h % 360 for h in hues)
|
|
154
|
+
widest = max([b - a for a, b in zip(ordered, ordered[1:])] + [ordered[0] + 360 - ordered[-1]])
|
|
155
|
+
return 360.0 if widest <= 180 else 2 * (360 - widest)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def collect_warnings(config: dict) -> list[str]:
|
|
159
|
+
notes: list[str] = []
|
|
160
|
+
colors = config.get("colors", [])
|
|
161
|
+
hues = [float(color.get("oklch", {}).get("h", 0)) for color in colors]
|
|
162
|
+
if len(hues) >= 3:
|
|
163
|
+
travel, best = hue_travel(hues), best_hue_travel(hues)
|
|
164
|
+
if travel > best * 1.1:
|
|
165
|
+
notes.append(
|
|
166
|
+
f"colors are not in hue order: neighbor hue steps add up to {travel:.0f} degrees, "
|
|
167
|
+
f"the best order gives {best:.0f}. The renderer mixes each color with its array "
|
|
168
|
+
"neighbors, and near-complementary neighbors mix toward gray."
|
|
169
|
+
)
|
|
170
|
+
return notes
|
|
171
|
+
|
|
172
|
+
|
|
146
173
|
def main() -> None:
|
|
147
174
|
parser = argparse.ArgumentParser()
|
|
148
175
|
parser.add_argument("config", type=pathlib.Path)
|
|
@@ -162,7 +189,11 @@ def main() -> None:
|
|
|
162
189
|
if errors:
|
|
163
190
|
print(json.dumps({"status": "invalid", "errors": errors}, ensure_ascii=False, indent=2))
|
|
164
191
|
sys.exit(1)
|
|
165
|
-
|
|
192
|
+
report = {"status": "valid", "mode": config["mode"], "preset": config["preset"], "colors": len(config["colors"])}
|
|
193
|
+
notes = collect_warnings(config)
|
|
194
|
+
if notes:
|
|
195
|
+
report["warnings"] = notes
|
|
196
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
166
197
|
|
|
167
198
|
|
|
168
199
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "scripts"))
|
|
10
|
+
import measure_preview as mp # noqa: E402
|
|
11
|
+
|
|
12
|
+
LAB_TO_LMS = np.array([
|
|
13
|
+
[1.0, 0.3963377774, 0.2158037573],
|
|
14
|
+
[1.0, -0.1055613458, -0.0638541728],
|
|
15
|
+
[1.0, -0.0894841775, -1.2914855480],
|
|
16
|
+
])
|
|
17
|
+
LMS_TO_RGB = np.array([
|
|
18
|
+
[4.0767416621, -3.3077115913, 0.2309699292],
|
|
19
|
+
[-1.2684380046, 2.6097574011, -0.3413193965],
|
|
20
|
+
[-0.0041960863, -0.7034186147, 1.7076147010],
|
|
21
|
+
])
|
|
22
|
+
HUES = (("rose", 18), ("peach", 62), ("mint", 159), ("cyan", 201), ("blue", 248), ("lilac", 304))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def lab_to_srgb(lab):
|
|
26
|
+
linear = np.clip(LMS_TO_RGB @ ((LAB_TO_LMS @ np.asarray(lab, dtype=np.float64)) ** 3), 0.0, 1.0)
|
|
27
|
+
return np.where(linear <= 0.0031308, linear * 12.92, 1.055 * linear ** (1 / 2.4) - 0.055)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def config(base=(0.982, 0.004, 94)):
|
|
31
|
+
return {
|
|
32
|
+
"mode": "opal",
|
|
33
|
+
"base": {"oklch": {"l": base[0], "c": base[1], "h": base[2]}},
|
|
34
|
+
"colors": [
|
|
35
|
+
{"id": cid, "label": cid, "oklch": {"l": 0.93, "c": 0.045, "h": h}, "intensity": 0.5, "peakOpacity": 0.08}
|
|
36
|
+
for cid, h in HUES
|
|
37
|
+
],
|
|
38
|
+
"field": {},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def base_lab(cfg):
|
|
43
|
+
o = cfg["base"]["oklch"]
|
|
44
|
+
h = np.radians(o["h"])
|
|
45
|
+
return np.array([o["l"], o["c"] * np.cos(h), o["c"] * np.sin(h)])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def flat(lab, size=(64, 64)):
|
|
49
|
+
return np.broadcast_to(lab_to_srgb(lab), (*size, 3)).copy()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MeasurePreviewTest(unittest.TestCase):
|
|
53
|
+
def setUp(self):
|
|
54
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
55
|
+
self.addCleanup(self.tmp.cleanup)
|
|
56
|
+
|
|
57
|
+
def measure_codes(self, cfg, codes):
|
|
58
|
+
path = pathlib.Path(self.tmp.name) / "field.png"
|
|
59
|
+
Image.fromarray(np.clip(codes, 0, 255).astype(np.uint8)).save(path)
|
|
60
|
+
return mp.measure(path, cfg)
|
|
61
|
+
|
|
62
|
+
def measure(self, cfg, pixels):
|
|
63
|
+
return self.measure_codes(cfg, np.round(pixels * 255))
|
|
64
|
+
|
|
65
|
+
def test_flat_base_reads_as_base(self):
|
|
66
|
+
for chroma in (0.0, 0.004):
|
|
67
|
+
with self.subTest(chroma=chroma):
|
|
68
|
+
cfg = config(base=(0.982, chroma, 94))
|
|
69
|
+
m = self.measure(cfg, flat(base_lab(cfg)))
|
|
70
|
+
self.assertEqual(m["chromaticPixelRatio"], 0)
|
|
71
|
+
self.assertEqual(m["grayRatio"], 0)
|
|
72
|
+
self.assertLess(abs(m["lightnessShift"]["mean"]), 0.0025)
|
|
73
|
+
self.assertIsNone(m["luminance"]["configuredCap"])
|
|
74
|
+
|
|
75
|
+
def test_visibility_does_not_depend_on_base_chroma(self):
|
|
76
|
+
tint = np.array([0.0, -0.0045, -0.0025])
|
|
77
|
+
ratios = []
|
|
78
|
+
for chroma in (0.0, 0.004, 0.008):
|
|
79
|
+
cfg = config(base=(0.982, chroma, 94))
|
|
80
|
+
ratios.append(self.measure(cfg, flat(base_lab(cfg) + tint))["chromaticPixelRatio"])
|
|
81
|
+
self.assertEqual(ratios, [1.0, 1.0, 1.0])
|
|
82
|
+
|
|
83
|
+
def test_hue_follows_deviation_direction(self):
|
|
84
|
+
cfg = config()
|
|
85
|
+
blue = np.radians(248)
|
|
86
|
+
m = self.measure(cfg, flat(base_lab(cfg) + 0.006 * np.array([0.0, np.cos(blue), np.sin(blue)])))
|
|
87
|
+
coverage = {c["id"]: c["measuredCoverage"] for c in m["colors"]}
|
|
88
|
+
self.assertEqual(max(coverage, key=coverage.get), "blue")
|
|
89
|
+
|
|
90
|
+
def test_lightness_only_shift_is_gray(self):
|
|
91
|
+
cfg = config(base=(0.982, 0.0, 94))
|
|
92
|
+
m = self.measure(cfg, flat(base_lab(cfg) + np.array([-0.01, 0.0, 0.0])))
|
|
93
|
+
self.assertEqual(m["grayRatio"], 1.0)
|
|
94
|
+
self.assertAlmostEqual(m["lightnessShift"]["mean"], -0.01, delta=0.002)
|
|
95
|
+
|
|
96
|
+
def test_tinted_shift_is_not_gray(self):
|
|
97
|
+
cfg = config()
|
|
98
|
+
rose = np.radians(18)
|
|
99
|
+
m = self.measure(cfg, flat(base_lab(cfg) + np.array([-0.006, 0.008 * np.cos(rose), 0.008 * np.sin(rose)])))
|
|
100
|
+
self.assertEqual(m["grayRatio"], 0)
|
|
101
|
+
self.assertEqual(m["chromaticPixelRatio"], 1.0)
|
|
102
|
+
|
|
103
|
+
def test_gray_ratio_is_only_defined_for_opal(self):
|
|
104
|
+
cfg = config(base=(0.098, 0.012, 236))
|
|
105
|
+
cfg["mode"] = "obsidian"
|
|
106
|
+
m = self.measure(cfg, flat(base_lab(cfg) + np.array([0.15, 0.0, 0.0])))
|
|
107
|
+
self.assertIsNone(m["grayRatio"])
|
|
108
|
+
|
|
109
|
+
def test_block_average_absorbs_dither(self):
|
|
110
|
+
cfg = config()
|
|
111
|
+
rng = np.random.default_rng(7)
|
|
112
|
+
codes = np.round(flat(base_lab(cfg)) * 255) + rng.integers(-1, 2, size=(64, 64, 3))
|
|
113
|
+
m = self.measure_codes(cfg, codes)
|
|
114
|
+
self.assertEqual(m["chromaticPixelRatio"], 0)
|
|
115
|
+
self.assertEqual(m["grayRatio"], 0)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import http.server
|
|
3
|
+
import pathlib
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import threading
|
|
7
|
+
import unittest
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "scripts"))
|
|
11
|
+
import serve_preview # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QuietHandler(serve_preview.NoStoreHandler):
|
|
15
|
+
def log_message(self, *args):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ServePreviewTest(unittest.TestCase):
|
|
20
|
+
def test_responses_are_not_cached(self):
|
|
21
|
+
body = b"window.SPECTRAL_THEME = {};"
|
|
22
|
+
with tempfile.TemporaryDirectory() as root:
|
|
23
|
+
pathlib.Path(root, "theme-config.js").write_bytes(body)
|
|
24
|
+
handler = functools.partial(QuietHandler, directory=root)
|
|
25
|
+
with http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) as server:
|
|
26
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
27
|
+
try:
|
|
28
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
29
|
+
url = f"http://127.0.0.1:{server.server_address[1]}/theme-config.js"
|
|
30
|
+
with opener.open(url) as response:
|
|
31
|
+
self.assertEqual(response.headers["Cache-Control"], "no-store")
|
|
32
|
+
self.assertEqual(response.read(), body)
|
|
33
|
+
finally:
|
|
34
|
+
server.shutdown()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
unittest.main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
7
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
8
|
+
import validate_manifest as vm # noqa: E402
|
|
9
|
+
|
|
10
|
+
HUES = {"rose": 18, "peach": 62, "mint": 159, "cyan": 201, "blue": 248, "lilac": 304}
|
|
11
|
+
INTERLEAVED = ["rose", "cyan", "lilac", "peach", "mint", "blue"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def manifest(mode="opal", order=None, cap=None):
|
|
15
|
+
field = {"scale": 1, "octaves": 4, "warpStrength": 0.3, "motionSpeed": 0.01, "staticTime": 1, "ditherStrength": 1}
|
|
16
|
+
if cap is not None:
|
|
17
|
+
field["luminanceCap"] = cap
|
|
18
|
+
return {
|
|
19
|
+
"schemaVersion": "1.1",
|
|
20
|
+
"mode": mode,
|
|
21
|
+
"label": "test",
|
|
22
|
+
"preset": "test",
|
|
23
|
+
"seed": 1,
|
|
24
|
+
"overallColorIntensity": 0.5,
|
|
25
|
+
"base": {"oklch": {"l": 0.98, "c": 0.004, "h": 94}},
|
|
26
|
+
"colors": [
|
|
27
|
+
{
|
|
28
|
+
"id": cid,
|
|
29
|
+
"label": cid,
|
|
30
|
+
"oklch": {"l": 0.93, "c": 0.04, "h": HUES[cid]},
|
|
31
|
+
"srgbFallback": "#ffffff",
|
|
32
|
+
"intensity": 0.5,
|
|
33
|
+
"peakOpacity": 0.08,
|
|
34
|
+
"fieldScale": 1,
|
|
35
|
+
"phase": [0, 0],
|
|
36
|
+
}
|
|
37
|
+
for cid in (order or list(HUES))
|
|
38
|
+
],
|
|
39
|
+
"field": field,
|
|
40
|
+
"output": {"colorSpace": "srgb", "reducedMotion": "frozen-calibrated-frame"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ValidateManifestTest(unittest.TestCase):
|
|
45
|
+
def test_hue_ordered_palette_passes_without_warnings(self):
|
|
46
|
+
self.assertEqual(vm.validate(manifest()), [])
|
|
47
|
+
self.assertEqual(vm.collect_warnings(manifest()), [])
|
|
48
|
+
|
|
49
|
+
def test_interleaved_palette_warns(self):
|
|
50
|
+
notes = vm.collect_warnings(manifest(order=INTERLEAVED))
|
|
51
|
+
self.assertEqual(len(notes), 1)
|
|
52
|
+
self.assertIn("hue order", notes[0])
|
|
53
|
+
|
|
54
|
+
def test_luminance_cap_is_required_only_for_obsidian(self):
|
|
55
|
+
self.assertEqual(vm.validate(manifest(mode="opal")), [])
|
|
56
|
+
errors = vm.validate(manifest(mode="obsidian"))
|
|
57
|
+
self.assertTrue(
|
|
58
|
+
any(msg.startswith("field.luminanceCap must be in (0, 1]") for msg in errors),
|
|
59
|
+
errors,
|
|
60
|
+
)
|
|
61
|
+
self.assertEqual(vm.validate(manifest(mode="obsidian", cap=0.165)), [])
|
|
62
|
+
|
|
63
|
+
@unittest.skipUnless(shutil.which("node"), "node is needed to load the starter manifests")
|
|
64
|
+
def test_starter_manifests(self):
|
|
65
|
+
for mode in ("opal", "obsidian"):
|
|
66
|
+
with self.subTest(mode=mode):
|
|
67
|
+
config = vm.load_manifest(ROOT / "assets" / "starter" / mode / "theme-config.js")
|
|
68
|
+
self.assertEqual(vm.validate(config), [])
|
|
69
|
+
self.assertEqual(vm.collect_warnings(config), [])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
unittest.main()
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"react": "^19.2.8",
|
|
13
|
-
"react-dom": "^19.
|
|
13
|
+
"react-dom": "^19.3.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/react": "^19.2.7",
|
|
@@ -814,24 +814,24 @@
|
|
|
814
814
|
}
|
|
815
815
|
},
|
|
816
816
|
"node_modules/react": {
|
|
817
|
-
"version": "19.
|
|
818
|
-
"resolved": "https://registry.npmjs.org/react/-/react-19.
|
|
819
|
-
"integrity": "sha512-
|
|
817
|
+
"version": "19.3.0",
|
|
818
|
+
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
|
819
|
+
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
|
820
820
|
"license": "MIT",
|
|
821
821
|
"engines": {
|
|
822
822
|
"node": ">=0.10.0"
|
|
823
823
|
}
|
|
824
824
|
},
|
|
825
825
|
"node_modules/react-dom": {
|
|
826
|
-
"version": "19.
|
|
827
|
-
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.
|
|
828
|
-
"integrity": "sha512-
|
|
826
|
+
"version": "19.3.0",
|
|
827
|
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
|
828
|
+
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
|
|
829
829
|
"license": "MIT",
|
|
830
830
|
"dependencies": {
|
|
831
|
-
"scheduler": "^0.
|
|
831
|
+
"scheduler": "^0.28.0"
|
|
832
832
|
},
|
|
833
833
|
"peerDependencies": {
|
|
834
|
-
"react": "^19.
|
|
834
|
+
"react": "^19.3.0"
|
|
835
835
|
}
|
|
836
836
|
},
|
|
837
837
|
"node_modules/rolldown": {
|
|
@@ -869,9 +869,9 @@
|
|
|
869
869
|
}
|
|
870
870
|
},
|
|
871
871
|
"node_modules/scheduler": {
|
|
872
|
-
"version": "0.
|
|
873
|
-
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.
|
|
874
|
-
"integrity": "sha512-
|
|
872
|
+
"version": "0.28.0",
|
|
873
|
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
|
874
|
+
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
|
875
875
|
"license": "MIT"
|
|
876
876
|
},
|
|
877
877
|
"node_modules/source-map-js": {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: meeting-notes
|
|
3
|
+
description: "Turns raw meeting notes into structured minutes: conclusion first, then decisions / action items / open questions; every action item must have an owner and a deadline. Use when the user pastes meeting transcripts or rough notes and asks for minutes or a summary."
|
|
4
|
+
category: productivity
|
|
5
|
+
risk: safe
|
|
6
|
+
source: https://github.com/alapha888/agent-skills-en
|
|
7
|
+
source_repo: alapha888/agent-skills-en
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-10-01"
|
|
10
|
+
author: alapha888
|
|
11
|
+
tags: [meetings, minutes, action-items]
|
|
12
|
+
tools: [claude, cursor, gemini, codex]
|
|
13
|
+
license: MIT
|
|
14
|
+
license_source: https://github.com/alapha888/agent-skills-en/blob/main/LICENSE
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Meeting Notes
|
|
18
|
+
|
|
19
|
+
Turn messy meeting notes into minutes that let someone who missed the meeting catch up in 3 minutes — and let attendees know exactly what they owe.
|
|
20
|
+
|
|
21
|
+
## When to Use
|
|
22
|
+
|
|
23
|
+
- Use when the user pastes meeting transcripts or rough notes and asks for minutes or a summary.
|
|
24
|
+
- Use when decisions and action items need owners and deadlines assigned.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. **Read the raw notes**: read the transcript or jotted notes the user pasted. Find "what was finally decided" first, then look back for the reasoning. Minutes record conclusions and points of disagreement, not the full discussion.
|
|
29
|
+
2. **Sort into three buckets**:
|
|
30
|
+
- **Decisions**: one per line, phrased as "Decided to do X", never "Discussed X".
|
|
31
|
+
- **Action items**: each must have an owner + a deadline. If the notes name no owner, mark `[unassigned]`; if no deadline, mark `[TBD]` — never invent them.
|
|
32
|
+
- **Open questions**: issues left undecided that need a follow-up; state "what is blocking, and who owns the next step."
|
|
33
|
+
3. **Conclusion first**: open the minutes with ≤3 lines on "what this meeting decided", then expand the three buckets.
|
|
34
|
+
4. **Self-check before output**: does every action item have an owner? A deadline? Are decisions affirmative statements? Do open questions have a next step? Flag any "no" — don't gloss over it.
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- Do not fabricate. Names, dates, and numbers not in the notes must not appear; mark missing ones `[to confirm]`.
|
|
39
|
+
- Do not editorialize. No "Maria's proposal seemed shaky" — only "Maria's proposal was rejected because…" (if a reason was stated).
|
|
40
|
+
- Length control: the minutes body must be ≤1/3 of the raw notes. Longer means you didn't distill — redo it.
|
|
41
|
+
|
|
42
|
+
## Minimal example
|
|
43
|
+
|
|
44
|
+
Input (raw notes excerpt):
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
...James said the login page is too slow, Maria suggested adding a cache layer, everyone agreed it's worth a pilot; draft the proposal by next Wednesday; pricing strategy still open, invite Finance next time...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Output:
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
## Meeting minutes (example)
|
|
54
|
+
|
|
55
|
+
**Bottom line**: Pilot a cache layer to speed up the login page; pricing strategy undecided, pending a session with Finance.
|
|
56
|
+
|
|
57
|
+
### Decisions
|
|
58
|
+
- Adopt a cache layer for login-page performance (pilot first, not a full rollout).
|
|
59
|
+
|
|
60
|
+
### Action items
|
|
61
|
+
- [ ] Maria: draft the caching proposal, due next Wednesday `[date to confirm]`
|
|
62
|
+
- [ ] [unassigned]: schedule pricing discussion with Finance `[time TBD]`
|
|
63
|
+
|
|
64
|
+
### Open questions
|
|
65
|
+
- Pricing strategy: no decision reached; the action-item owner will book a dedicated session with Finance.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
- Works only from notes the user supplies. It cannot join the meeting, recover audio, or resolve a name the notes never recorded.
|
|
71
|
+
- Missing owners and deadlines are marked `[unassigned]` / `[TBD]`, never invented; the minutes are therefore only as complete as the raw notes.
|
|
72
|
+
- It records decisions, not consensus. If the notes only say something was "discussed", the skill reports that as an open question instead of a decision.
|
|
73
|
+
- Summaries bias toward brevity (≤1/3 of the raw notes), so fine-grained discussion detail is intentionally dropped.
|
|
74
|
+
|
|
75
|
+
## Anti-patterns
|
|
76
|
+
|
|
77
|
+
- ❌ Chronological minutes: replaying two hours of discussion in order is the same as not organizing it.
|
|
78
|
+
- ❌ Ownerless action items: "everyone please follow up" — an action item without an owner and a deadline is not an action item.
|
|
79
|
+
- ❌ Invented details: guessing dates or names the notes never mentioned turns minutes into fiction.
|
|
80
|
+
- ❌ Decisions written as discussion: "Discussed the feasibility of a caching layer" — did the meeting adopt it or not? Minutes need affirmative statements.
|