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.

Potentially problematic release.


This version of opencode-skills-collection might be problematic. Click here for more details.

Files changed (99) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +31 -1
  2. package/bundled-skills/ai-agent-evaluation-benchmarking/SKILL.md +124 -0
  3. package/bundled-skills/ai-agent-tool-routing/SKILL.md +136 -0
  4. package/bundled-skills/ai-code-generation-guardrails/SKILL.md +142 -0
  5. package/bundled-skills/ai-prompt-regression-testing/SKILL.md +130 -0
  6. package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +26 -8
  7. package/bundled-skills/cline-pilot/README.md +138 -0
  8. package/bundled-skills/cline-pilot/SKILL.md +301 -0
  9. package/bundled-skills/cline-pilot/assets/global-memory-bank-prompt.en.md +66 -0
  10. package/bundled-skills/cline-pilot/assets/global-memory-bank-prompt.md +56 -0
  11. package/bundled-skills/cline-pilot/references/cold-start.md +58 -0
  12. package/bundled-skills/cline-pilot/references/decision-log.example.md +9 -0
  13. package/bundled-skills/cline-pilot/references/local-config.example.md +26 -0
  14. package/bundled-skills/cline-pilot/references/project-profiles.example.md +10 -0
  15. package/bundled-skills/cline-pilot/scripts/session_report.py +151 -0
  16. package/bundled-skills/cross-chain-relayer-audit/SKILL.md +136 -0
  17. package/bundled-skills/deep-research-framework/SKILL.md +81 -0
  18. package/bundled-skills/defi-yield-strategy-allocator/SKILL.md +124 -0
  19. package/bundled-skills/dropthehassle-publish/SKILL.md +151 -0
  20. package/bundled-skills/five-axis-code-review/SKILL.md +78 -0
  21. package/bundled-skills/git-commit-message/SKILL.md +76 -0
  22. package/bundled-skills/idea-to-blueprint/LICENSE +21 -0
  23. package/bundled-skills/idea-to-blueprint/SKILL.md +260 -0
  24. package/bundled-skills/idea-to-blueprint/assets/AGENTS.md.template +38 -0
  25. package/bundled-skills/idea-to-blueprint/assets/CLAUDE.md.template +38 -0
  26. package/bundled-skills/idea-to-blueprint/assets/DECISIONS.md.template +14 -0
  27. package/bundled-skills/idea-to-blueprint/assets/PROGRESS.md.template +47 -0
  28. package/bundled-skills/idea-to-blueprint/assets/session-prompt.md +23 -0
  29. package/bundled-skills/idea-to-blueprint/references/architecture-decisions.md +202 -0
  30. package/bundled-skills/idea-to-blueprint/references/blueprint-template.md +259 -0
  31. package/bundled-skills/idea-to-blueprint/references/code-quality-and-review.md +216 -0
  32. package/bundled-skills/idea-to-blueprint/references/design-minimal.md +96 -0
  33. package/bundled-skills/idea-to-blueprint/references/edge-case-catalog.md +239 -0
  34. package/bundled-skills/idea-to-blueprint/references/intake-questions.md +105 -0
  35. package/bundled-skills/idea-to-blueprint/references/research-protocol.md +196 -0
  36. package/bundled-skills/idea-to-blueprint/references/session-protocol.md +133 -0
  37. package/bundled-skills/idea-to-blueprint/references/stories-ac-tests.md +194 -0
  38. package/bundled-skills/idea-to-blueprint/references/ux-writing-guide.md +128 -0
  39. package/bundled-skills/idea-to-blueprint/scripts/lint_blueprint.py +460 -0
  40. package/bundled-skills/liuguang-banlan-ui/SKILL.md +22 -18
  41. package/bundled-skills/liuguang-banlan-ui/assets/starter/launcher.css +2 -2
  42. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/index.html +5 -4
  43. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme-config.js +3 -10
  44. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme.css +48 -62
  45. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/index.html +5 -4
  46. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme-config.js +27 -35
  47. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme.css +48 -62
  48. package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/spectral-field.js +135 -148
  49. package/bundled-skills/liuguang-banlan-ui/references/style-contract.md +26 -7
  50. package/bundled-skills/liuguang-banlan-ui/references/verification.md +19 -7
  51. package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +78 -47
  52. package/bundled-skills/liuguang-banlan-ui/scripts/serve_preview.py +35 -0
  53. package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +34 -3
  54. package/bundled-skills/liuguang-banlan-ui/tests/test_measure_preview.py +119 -0
  55. package/bundled-skills/liuguang-banlan-ui/tests/test_serve_preview.py +38 -0
  56. package/bundled-skills/liuguang-banlan-ui/tests/test_validate_manifest.py +73 -0
  57. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +12 -12
  58. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +1 -1
  59. package/bundled-skills/meeting-notes/SKILL.md +80 -0
  60. package/bundled-skills/repo-foundation/SKILL.md +166 -0
  61. package/bundled-skills/repo-foundation/references/LICENSE.md +21 -0
  62. package/bundled-skills/repo-foundation/references/bootstrap.md +63 -0
  63. package/bundled-skills/repo-foundation/references/companion.md +9 -0
  64. package/bundled-skills/repo-foundation/references/continuity.md +50 -0
  65. package/bundled-skills/repo-foundation/references/evolution.md +60 -0
  66. package/bundled-skills/repo-foundation/references/migration-examples.md +53 -0
  67. package/bundled-skills/repo-foundation/references/shared-contracts.md +48 -0
  68. package/bundled-skills/repo-foundation/references/verification.md +50 -0
  69. package/bundled-skills/repo-native-refactor/SKILL.md +225 -0
  70. package/bundled-skills/repo-native-refactor/references/LICENSE.md +21 -0
  71. package/bundled-skills/repo-native-refactor/references/deterministic-tooling.md +101 -0
  72. package/bundled-skills/repo-native-refactor/references/error-reliability.md +117 -0
  73. package/bundled-skills/repo-native-refactor/references/finding-taxonomy.md +181 -0
  74. package/bundled-skills/repo-native-refactor/references/refactor-examples.md +77 -0
  75. package/bundled-skills/repo-native-refactor/references/repository-prose.md +157 -0
  76. package/bundled-skills/repo-native-refactor/references/repository-rehabilitation.md +94 -0
  77. package/bundled-skills/repo-native-refactor/references/semantic-risk.md +187 -0
  78. package/bundled-skills/repo-native-refactor/references/shared-contracts.md +48 -0
  79. package/bundled-skills/repo-native-refactor/references/testing-integrity.md +103 -0
  80. package/bundled-skills/smart-contract-formal-verification/SKILL.md +136 -0
  81. package/bundled-skills/smart-contract-upgrade-governance/SKILL.md +130 -0
  82. package/bundled-skills/soroban-contract-audit/SKILL.md +136 -0
  83. package/bundled-skills/soroban-liquidity-pool/SKILL.md +118 -0
  84. package/bundled-skills/soroban-oracle-data-feed-audit/SKILL.md +118 -0
  85. package/bundled-skills/soroban-storage-ttl-lifecycle/SKILL.md +118 -0
  86. package/bundled-skills/soroban-token-minter/SKILL.md +124 -0
  87. package/bundled-skills/stellar-anchor-integration/SKILL.md +136 -0
  88. package/bundled-skills/stellar-asset-clawback-compliance/SKILL.md +118 -0
  89. package/bundled-skills/stellar-escrow-timelock/SKILL.md +118 -0
  90. package/bundled-skills/stellar-multisig-threshold-coordinator/SKILL.md +112 -0
  91. package/bundled-skills/tech-writing-proofread/SKILL.md +78 -0
  92. package/bundled-skills/typescript-expert/scripts/ts_diagnostic.py +40 -19
  93. package/bundled-skills/waf-setup/SKILL.md +1 -1
  94. package/bundled-skills/web3-rate-limiting-circuit-breaker/SKILL.md +130 -0
  95. package/bundled-skills/web3-transaction-relayer-pool/SKILL.md +124 -0
  96. package/bundled-skills/youtube-summarizer/SKILL.md +3 -3
  97. package/bundled-skills/zk-proof-verification-pipeline/SKILL.md +124 -0
  98. package/package.json +1 -1
  99. package/skills_index.json +1059 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env python3
2
+ """Lint a generated blueprint (.md) for the structural guarantees the build sessions rely on.
3
+
4
+ Usage:
5
+ python3 lint_blueprint.py path/to/blueprint.md [--strict]
6
+
7
+ Exit code 0 = no errors (warnings may remain), 1 = errors found (or warnings with --strict).
8
+
9
+ What it checks (E = error, W = warning):
10
+ E required top-level sections present (by English anchor in a `## ` heading)
11
+ E no placeholder text (TODO, TBD, TBA, FIXME, XXX, lorem, ???, [insert, to be defined/decided)
12
+ E at least one `## Epic nn — Title` and Epic 00 present
13
+ E each epic has **Definition of Done**; W each epic has **Session handoff checklist**
14
+ E story headings `### Enn-Snn — Title` are unique and live inside the matching epic
15
+ E each story has **Acceptance Criteria**, **Edge cases**, **Tests**, at least one `AC-n`, and names a persona `P<n>`
16
+ W each story has **Story**, **Context**, **Scope**, **Tasks**, **Story DoD**
17
+ E no "As a user" stories
18
+ E every `[UNKNOWN → OQ-nn]` has a matching OQ-nn row in the Open Questions section; bare `[UNKNOWN]` without an OQ is an error
19
+ E every table body row in the Tech Stack section carries [VERIFIED / [ASSUMED / [STATED (or [UNVERIFIED], which also raises W)
20
+ E every S-nn cited in a [VERIFIED …] tag exists in the Sources section; W fewer than 5 URLs in Sources
21
+ E every defined persona P<n> is used by at least one story
22
+ W copy IDs (CP-…) referenced in stories are defined in the Copy section
23
+ W dangling story references (Enn-Snn mentioned but not defined)
24
+ W `## ` headings with non-ASCII text carry an ASCII anchor in parentheses
25
+ W vague adjectives inside AC lines (properly, appropriately, fast, quickly, easily, seamless, user-friendly, intuitive)
26
+ W Session Protocol section mentions an Amendments log; W an appendix with session prompts exists
27
+ E Architecture section defines drivers AD-n and mentions fitness functions; W <3 drivers, drivers
28
+ with no number in them, no explicit non-drivers
29
+ E every AD-n referenced outside the Architecture section is defined inside it
30
+ W Tech Stack table has a runner-up column (a choice with no stated cost was not a decision)
31
+ E Quality section names a `check` command and embeds >=10 review-checklist items of the form `[ ] …`
32
+ W Quality section names critical e2e paths, says something about mutation testing, has M-nn metric
33
+ rows, and lists anti-metrics
34
+ E no metric row defines an output-per-person metric (lines of code, commits/PRs/diffs per engineer,
35
+ story points as productivity); W coverage appearing as a metric without the word "floor"
36
+
37
+ Add `<!-- lint-ignore -->` at the end of a line to suppress placeholder/adjective findings on that line
38
+ (e.g., the rule sentence "No TODO or TBD anywhere" in the How-to-use section).
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import re
43
+ import sys
44
+ from dataclasses import dataclass, field
45
+
46
+ REQUIRED_SECTIONS_ERROR = [
47
+ "How to use", "Executive summary", "Scope", "Research", "Assumptions", "Personas",
48
+ "Goals", "Architecture", "Tech Stack", "Quality", "Edge Cases", "Delivery Plan",
49
+ "Epics", "Session Protocol", "Risks", "Open Questions", "Glossary", "Sources",
50
+ ]
51
+ REQUIRED_SECTIONS_WARN = ["Flows", "Screens", "Design", "UX Writing", "Copy"]
52
+
53
+ PLACEHOLDER_RE = re.compile(
54
+ r"\bTODO\b|\bTBD\b|\bTBA\b|\bFIXME\b|\bXXX\b|lorem ipsum|\?\?\?|\[insert|to be defined|to be decided",
55
+ re.IGNORECASE,
56
+ )
57
+ EPIC_HEADING_RE = re.compile(r"^## Epic (\d{2})\s*[—–-]\s*(.+?)\s*$")
58
+ STORY_HEADING_RE = re.compile(r"^### (E(\d{2})-S(\d{2}))\s*[—–-]\s*(.+?)\s*$")
59
+ STORY_REF_RE = re.compile(r"\bE\d{2}-S\d{2}\b")
60
+ AC_LINE_RE = re.compile(r"^\s*[-*]\s*AC-\d+", re.IGNORECASE)
61
+ PERSONA_REF_RE = re.compile(r"(?<![A-Za-z])P(\d+)\b")
62
+ UNKNOWN_RE = re.compile(r"\[UNKNOWN([^\]]*)\]")
63
+ OQ_IN_UNKNOWN_RE = re.compile(r"OQ-(\d+)")
64
+ VERIFIED_SOURCE_RE = re.compile(r"\[VERIFIED[^\]]*?\bS-(\d+)")
65
+ SOURCE_ID_RE = re.compile(r"\bS-(\d+)\b")
66
+ URL_RE = re.compile(r"https?://\S+")
67
+ CP_ID_RE = re.compile(r"\bCP-[A-Za-z0-9]+-\d+\b")
68
+ EVIDENCE_TAG_RE = re.compile(r"\[(VERIFIED|ASSUMED|STATED|UNVERIFIED)\b")
69
+ UNVERIFIED_RE = re.compile(r"\[UNVERIFIED\]")
70
+ VAGUE_RE = re.compile(
71
+ r"\b(properly|appropriately|fast|quickly|easily|seamless(?:ly)?|user-friendly|intuitive(?:ly)?)\b",
72
+ re.IGNORECASE,
73
+ )
74
+ AS_A_USER_RE = re.compile(r"\bAs an? user\b", re.IGNORECASE)
75
+ AD_ID_RE = re.compile(r"\bAD-(\d+)\b")
76
+ DIGIT_RE = re.compile(r"\d")
77
+ FITNESS_RE = re.compile(r"fitness function", re.IGNORECASE)
78
+ CHECK_CMD_RE = re.compile(r"`[^`\n]*\bcheck\b[^`\n]*`")
79
+ CHECKLIST_ITEM_RE = re.compile(r"^\s*(?:[-*]\s*)?\[ \]\s+\S")
80
+ METRIC_ROW_RE = re.compile(r"\bM-\d+(?:\.\d+)?\b")
81
+ BANNED_METRIC_RE = re.compile(
82
+ r"lines of code|\bLOC\b|commits? per (?:engineer|developer|dev|person)"
83
+ r"|(?:pull requests?|PRs?|diffs?) per (?:engineer|developer|dev|person)"
84
+ r"|story points? (?:per|as a measure|completed per)|velocity per",
85
+ re.IGNORECASE,
86
+ )
87
+ COVERAGE_TARGET_RE = re.compile(r"coverage", re.IGNORECASE)
88
+ LINT_IGNORE = "<!-- lint-ignore -->"
89
+
90
+
91
+ @dataclass
92
+ class Findings:
93
+ errors: list[str] = field(default_factory=list)
94
+ warnings: list[str] = field(default_factory=list)
95
+
96
+ def err(self, line: int | None, msg: str) -> None:
97
+ self.errors.append(f"L{line}: {msg}" if line else msg)
98
+
99
+ def warn(self, line: int | None, msg: str) -> None:
100
+ self.warnings.append(f"L{line}: {msg}" if line else msg)
101
+
102
+
103
+ @dataclass
104
+ class Section:
105
+ title: str
106
+ start: int # 0-based index of heading line
107
+ end: int # exclusive
108
+
109
+
110
+ def level2_sections(lines: list[str]) -> list[Section]:
111
+ idx = [i for i, l in enumerate(lines) if l.startswith("## ")]
112
+ sections = []
113
+ for n, i in enumerate(idx):
114
+ end = idx[n + 1] if n + 1 < len(idx) else len(lines)
115
+ sections.append(Section(lines[i][3:].strip(), i, end))
116
+ return sections
117
+
118
+
119
+ def find_section(sections: list[Section], anchor: str) -> Section | None:
120
+ a = anchor.lower()
121
+ for s in sections:
122
+ if EPIC_HEADING_RE.match("## " + s.title):
123
+ continue
124
+ if a in s.title.lower():
125
+ return s
126
+ return None
127
+
128
+
129
+ def block_text(lines: list[str], start: int, end: int) -> str:
130
+ return "\n".join(lines[start:end])
131
+
132
+
133
+ def table_body_rows(lines: list[str], start: int, end: int) -> list[tuple[int, str]]:
134
+ """Return (line_no, row) for table rows that are neither header nor separator."""
135
+ rows = []
136
+ in_table = False
137
+ header_seen = False
138
+ for i in range(start, end):
139
+ l = lines[i].strip()
140
+ if l.startswith("|"):
141
+ if not in_table:
142
+ in_table, header_seen = True, True # first row of a table is its header
143
+ continue
144
+ if re.match(r"^\|[\s:\-|]+\|$", l):
145
+ continue # separator
146
+ rows.append((i + 1, l))
147
+ else:
148
+ in_table, header_seen = False, False
149
+ return rows
150
+
151
+
152
+ def main(argv: list[str]) -> int:
153
+ if len(argv) < 2 or argv[1] in {"-h", "--help"}:
154
+ print(__doc__)
155
+ return 0
156
+ path = argv[1]
157
+ strict = "--strict" in argv
158
+ with open(path, encoding="utf-8") as f:
159
+ lines = f.read().splitlines()
160
+ f_ = Findings()
161
+ sections = level2_sections(lines)
162
+
163
+ # 1. Required sections
164
+ for anchor in REQUIRED_SECTIONS_ERROR:
165
+ if not find_section(sections, anchor):
166
+ f_.err(None, f"Missing required section with anchor '{anchor}' in a '## ' heading")
167
+ for anchor in REQUIRED_SECTIONS_WARN:
168
+ if not find_section(sections, anchor):
169
+ f_.warn(None, f"No section with anchor '{anchor}' — fine only if it truly does not apply (say so in the doc)")
170
+
171
+ # 2. Placeholders + vague adjectives + "As a user"
172
+ for i, l in enumerate(lines, 1):
173
+ if LINT_IGNORE in l:
174
+ continue
175
+ m = PLACEHOLDER_RE.search(l)
176
+ if m:
177
+ f_.err(i, f"Placeholder text '{m.group(0)}' — decide it or route to an [UNKNOWN → OQ-nn]")
178
+ if AC_LINE_RE.match(l):
179
+ v = VAGUE_RE.search(l)
180
+ if v:
181
+ f_.warn(i, f"Vague word '{v.group(0)}' in an acceptance criterion — replace with a number or observable result")
182
+ if AS_A_USER_RE.search(l):
183
+ f_.err(i, "'As a user' — stories must name a persona ID and name (e.g. 'As **P2 (Owner)**')")
184
+
185
+ # 3. Epics and stories
186
+ epics: list[tuple[int, str, Section]] = [] # (epic_no, title, section)
187
+ for s in sections:
188
+ m = EPIC_HEADING_RE.match("## " + s.title)
189
+ if m:
190
+ epics.append((int(m.group(1)), m.group(2), s))
191
+ if not epics:
192
+ f_.err(None, "No epics found — expected headings like '## Epic 00 — Foundation'")
193
+ elif not any(n == 0 for n, _, _ in epics):
194
+ f_.err(None, "Epic 00 (Foundation) is missing")
195
+
196
+ story_ids: dict[str, int] = {}
197
+ story_blocks: list[tuple[str, int, int, int]] = [] # (id, epic_no, start, end)
198
+ for epic_no, epic_title, sec in epics:
199
+ text = block_text(lines, sec.start, sec.end)
200
+ if "**Definition of Done**" not in text:
201
+ f_.err(sec.start + 1, f"Epic {epic_no:02d} has no **Definition of Done**")
202
+ if "**Session handoff checklist**" not in text:
203
+ f_.warn(sec.start + 1, f"Epic {epic_no:02d} has no **Session handoff checklist**")
204
+ # stories inside this epic
205
+ heads = [i for i in range(sec.start + 1, sec.end) if lines[i].startswith("### ")]
206
+ if not heads:
207
+ f_.err(sec.start + 1, f"Epic {epic_no:02d} has no stories ('### Enn-Snn — Title')")
208
+ for n, i in enumerate(heads):
209
+ end = heads[n + 1] if n + 1 < len(heads) else sec.end
210
+ m = STORY_HEADING_RE.match(lines[i])
211
+ if not m:
212
+ f_.warn(i + 1, f"Level-3 heading inside Epic {epic_no:02d} is not a story heading: '{lines[i][4:60]}'")
213
+ continue
214
+ sid, e_no = m.group(1), int(m.group(2))
215
+ if sid in story_ids:
216
+ f_.err(i + 1, f"Duplicate story ID {sid} (first at L{story_ids[sid]})")
217
+ story_ids[sid] = i + 1
218
+ if e_no != epic_no:
219
+ f_.err(i + 1, f"Story {sid} is inside Epic {epic_no:02d} but its ID says E{e_no:02d}")
220
+ story_blocks.append((sid, epic_no, i, end))
221
+
222
+ all_story_text = ""
223
+ for sid, _, start, end in story_blocks:
224
+ text = block_text(lines, start, end)
225
+ all_story_text += "\n" + text
226
+ for label in ("**Acceptance Criteria**", "**Edge cases**", "**Tests**"):
227
+ if label not in text:
228
+ f_.err(start + 1, f"{sid} is missing {label}")
229
+ for label in ("**Story**", "**Context**", "**Scope**", "**Tasks**", "**Story DoD**"):
230
+ if label not in text:
231
+ f_.warn(start + 1, f"{sid} is missing {label}")
232
+ if not any(AC_LINE_RE.match(l) for l in lines[start:end]):
233
+ f_.err(start + 1, f"{sid} has no 'AC-n' lines")
234
+ if not PERSONA_REF_RE.search(text):
235
+ f_.err(start + 1, f"{sid} does not reference a persona ID (P1, P2, … or P0 for the builder)")
236
+
237
+ # dangling story references
238
+ defined = set(story_ids)
239
+ for i, l in enumerate(lines, 1):
240
+ for ref in STORY_REF_RE.findall(l):
241
+ if ref not in defined:
242
+ f_.warn(i, f"Reference to undefined story {ref}")
243
+
244
+ # 4. UNKNOWN → OQ
245
+ oq_sec = find_section(sections, "Open Questions")
246
+ oq_text = block_text(lines, oq_sec.start, oq_sec.end) if oq_sec else ""
247
+ for i, l in enumerate(lines, 1):
248
+ for m in UNKNOWN_RE.finditer(l):
249
+ oqs = OQ_IN_UNKNOWN_RE.findall(m.group(1))
250
+ if not oqs:
251
+ f_.err(i, "[UNKNOWN] without an OQ reference — use '[UNKNOWN → OQ-nn]'")
252
+ for q in oqs:
253
+ if oq_sec and not re.search(rf"\bOQ-{q}\b", oq_text):
254
+ f_.err(i, f"OQ-{q} referenced but not present in the Open Questions section")
255
+
256
+ # 5. Tech stack evidence
257
+ ts = find_section(sections, "Tech Stack")
258
+ if ts:
259
+ rows = table_body_rows(lines, ts.start, ts.end)
260
+ if not rows:
261
+ f_.err(ts.start + 1, "Tech Stack section has no table rows")
262
+ for ln, row in rows:
263
+ if not EVIDENCE_TAG_RE.search(row):
264
+ f_.err(ln, "Tech Stack table row without an evidence tag ([VERIFIED …] / [ASSUMED …] / [STATED])")
265
+
266
+ # 6. Sources
267
+ src = find_section(sections, "Sources")
268
+ if src:
269
+ src_text = block_text(lines, src.start, src.end)
270
+ defined_sources = set(SOURCE_ID_RE.findall(src_text))
271
+ urls = URL_RE.findall(src_text)
272
+ if len(urls) < 5:
273
+ f_.warn(src.start + 1, f"Only {len(urls)} URL(s) in Sources — deep research normally yields far more")
274
+ for i, l in enumerate(lines, 1):
275
+ if src.start < i - 1 < src.end:
276
+ continue
277
+ for s_id in VERIFIED_SOURCE_RE.findall(l):
278
+ if s_id not in defined_sources:
279
+ f_.err(i, f"[VERIFIED …] cites S-{s_id}, which is not in the Sources section")
280
+
281
+ # 7. Personas used
282
+ per = find_section(sections, "Personas")
283
+ if per:
284
+ per_text = block_text(lines, per.start, per.end)
285
+ defined_personas = sorted({int(x) for x in PERSONA_REF_RE.findall(per_text)})
286
+ if not defined_personas:
287
+ f_.err(per.start + 1, "Personas section defines no persona IDs (P1, P2, …)")
288
+ used = {int(x) for x in PERSONA_REF_RE.findall(all_story_text)}
289
+ for p in defined_personas:
290
+ if p not in used:
291
+ f_.err(per.start + 1, f"Persona P{p} is never referenced by any story — remove it or write its stories")
292
+
293
+ # 8. Copy IDs referenced vs defined
294
+ cp = find_section(sections, "Copy")
295
+ if cp:
296
+ cp_defined = set(CP_ID_RE.findall(block_text(lines, cp.start, cp.end)))
297
+ for sid, _, start, end in story_blocks:
298
+ for ref in set(CP_ID_RE.findall(block_text(lines, start, end))):
299
+ if ref not in cp_defined:
300
+ f_.warn(start + 1, f"{sid} references copy ID {ref} that is not defined in the Copy section")
301
+
302
+ # 9. Heading anchors
303
+ for s in sections:
304
+ if any(ord(ch) > 127 and ch.isalpha() for ch in s.title) and not re.search(r"\([A-Za-z][A-Za-z0-9 &/\-]*\)", s.title):
305
+ f_.warn(s.start + 1, f"Heading '{s.title[:40]}' has no ASCII anchor in parentheses")
306
+
307
+ # 10. Session protocol / appendix
308
+ sp = find_section(sections, "Session Protocol")
309
+ if sp and "amendment" not in block_text(lines, sp.start, sp.end).lower():
310
+ f_.warn(sp.start + 1, "Session Protocol section does not mention the Amendments log")
311
+ if not find_section(sections, "Session Prompts"):
312
+ f_.warn(None, "No appendix with 'Session Prompts' — the user needs one copy-paste prompt per epic")
313
+
314
+ # 11. UNVERIFIED tags (only legitimate when web tools were unavailable)
315
+ unverified = sum(len(UNVERIFIED_RE.findall(l)) for l in lines)
316
+ if unverified:
317
+ f_.warn(None, f"{unverified} [UNVERIFIED] tag(s) — acceptable only with the no-web-access warning on the cover; the first build session must re-verify each")
318
+
319
+ # 12. Architecture drivers and the style decision
320
+ arch = find_section(sections, "Architecture")
321
+ ad_defined: set[str] = set()
322
+ if arch:
323
+ arch_text = block_text(lines, arch.start, arch.end)
324
+ # A driver is *defined* where its ID opens the line (optionally after a list marker, bold
325
+ # marker or heading hashes) or opens a table row's first cell. Everywhere else is a reference.
326
+ for i in range(arch.start, min(arch.end, len(lines))):
327
+ l = lines[i]
328
+ head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
329
+ m = AD_ID_RE.match(head)
330
+ if m:
331
+ ad_defined.add(m.group(1))
332
+ if not ad_defined:
333
+ f_.err(
334
+ arch.start + 1,
335
+ "Architecture section defines no drivers (AD-1, AD-2, …) — the style decision has "
336
+ "nothing to be scored against, which is how a stack gets chosen first and justified after",
337
+ )
338
+ elif len(ad_defined) < 3:
339
+ f_.warn(arch.start + 1, f"Only {len(ad_defined)} architecture driver(s) — 5–8 ranked drivers is the target")
340
+ # Each driver definition line must carry a measurable target. Strip the AD-n IDs first:
341
+ # the digit in the ID itself does not make the driver measurable.
342
+ for i in range(arch.start, min(arch.end, len(lines))):
343
+ l = lines[i]
344
+ head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
345
+ if LINT_IGNORE in l or not AD_ID_RE.match(head):
346
+ continue
347
+ if not DIGIT_RE.search(AD_ID_RE.sub("", l)):
348
+ f_.warn(
349
+ i + 1,
350
+ "Architecture driver with no number in it — a driver without a measurable target "
351
+ "cannot decide anything ('scalable' is not a driver)",
352
+ )
353
+ if not FITNESS_RE.search(arch_text):
354
+ f_.err(
355
+ arch.start + 1,
356
+ "Architecture section never mentions fitness functions — boundaries that are described "
357
+ "but not enforced are gone within a few sessions",
358
+ )
359
+ if "non-driver" not in arch_text.lower() and "not optimis" not in arch_text.lower() and "not optimiz" not in arch_text.lower():
360
+ f_.warn(arch.start + 1, "No explicit non-drivers ('what we are NOT optimising for') in the Architecture section")
361
+
362
+ # Every AD-n referenced anywhere must be defined by a driver line in the Architecture section
363
+ if ad_defined:
364
+ for i, l in enumerate(lines, 1):
365
+ head = l.lstrip().lstrip("#*->|").lstrip().lstrip("*_ ")
366
+ if arch and arch.start < i - 1 < arch.end and AD_ID_RE.match(head):
367
+ continue # this is the definition line itself
368
+ for ad in AD_ID_RE.findall(l):
369
+ if ad not in ad_defined:
370
+ f_.err(i, f"AD-{ad} is referenced but never defined as a driver in the Architecture section")
371
+
372
+ # 13. Tech stack table shape — every row must name what the runner-up would have bought
373
+ if ts:
374
+ header = ""
375
+ for i in range(ts.start, min(ts.end, len(lines))):
376
+ if lines[i].lstrip().startswith("|") and "---" not in lines[i]:
377
+ header = lines[i].lower()
378
+ break
379
+ if header and "runner" not in header:
380
+ f_.warn(
381
+ ts.start + 1,
382
+ "Tech Stack table has no runner-up column — a choice with no stated cost was not a decision",
383
+ )
384
+
385
+ # 14. Quality section: the gate, the checklist, the critical paths
386
+ q = find_section(sections, "Quality")
387
+ if q:
388
+ q_lines = lines[q.start : q.end]
389
+ q_text = "\n".join(q_lines)
390
+ q_lower = q_text.lower()
391
+ if not CHECK_CMD_RE.search(q_text):
392
+ f_.err(
393
+ q.start + 1,
394
+ "Quality section names no `check` command in backticks — the build sessions have no "
395
+ "definition of green",
396
+ )
397
+ checklist_items = sum(1 for l in q_lines if CHECKLIST_ITEM_RE.match(l))
398
+ if checklist_items < 10:
399
+ f_.err(
400
+ q.start + 1,
401
+ f"Quality section contains {checklist_items} review-checklist item(s) of the form '[ ] …' — "
402
+ "embed the review checklist verbatim (code-quality-and-review.md §11) or the reviewer "
403
+ "invents a different standard every session",
404
+ )
405
+ if "e2e" not in q_lower and "end-to-end" not in q_lower:
406
+ f_.warn(q.start + 1, "Quality section does not name the critical e2e paths")
407
+ if "mutation" not in q_lower:
408
+ f_.warn(
409
+ q.start + 1,
410
+ "Quality section says nothing about mutation testing — name the tool for this stack, "
411
+ "or state that no maintained one exists",
412
+ )
413
+ if not METRIC_ROW_RE.search(q_text):
414
+ f_.warn(q.start + 1, "Quality section has no metrics table rows (M-nn) — nothing will be measured")
415
+ if "anti-metric" not in q_lower and "will not measure" not in q_lower and "not measure" not in q_lower:
416
+ f_.warn(
417
+ q.start + 1,
418
+ "Quality section lists no anti-metrics — say explicitly what will not be measured and why",
419
+ )
420
+
421
+ # 15. Banned productivity metrics inside metric definition rows
422
+ for i, l in enumerate(lines, 1):
423
+ if LINT_IGNORE in l or not METRIC_ROW_RE.search(l):
424
+ continue
425
+ m = BANNED_METRIC_RE.search(l)
426
+ if m:
427
+ f_.err(
428
+ i,
429
+ f"Metric row defines '{m.group(0)}' — output-per-person metrics are gamed the moment they "
430
+ "become targets; measure the system, not the author",
431
+ )
432
+ if COVERAGE_TARGET_RE.search(l) and "floor" not in l.lower():
433
+ f_.warn(
434
+ i,
435
+ "Coverage appears as a metric without the word 'floor' — state it as a regression floor, "
436
+ "never as a target to raise",
437
+ )
438
+
439
+ # 16. Report
440
+ print(f"Blueprint lint — {path}")
441
+ print(f" epics: {len(epics)} stories: {len(story_ids)} sections: {len(sections)}")
442
+ if f_.errors:
443
+ print(f"\nERRORS ({len(f_.errors)}):")
444
+ for e in f_.errors:
445
+ print(" ✘ " + e)
446
+ if f_.warnings:
447
+ print(f"\nWARNINGS ({len(f_.warnings)}):")
448
+ for w in f_.warnings:
449
+ print(" ! " + w)
450
+ if not f_.errors and not f_.warnings:
451
+ print("\nPASS — no findings.")
452
+ elif not f_.errors:
453
+ print("\nPASS with warnings — resolve or justify each warning in the document.")
454
+ else:
455
+ print("\nFAIL — fix every error before delivering the blueprint.")
456
+ return 1 if f_.errors or (strict and f_.warnings) else 0
457
+
458
+
459
+ if __name__ == "__main__":
460
+ sys.exit(main(sys.argv))
@@ -44,7 +44,7 @@ Read [style-contract.md](references/style-contract.md) before choosing a mode or
44
44
 
45
45
  - Choose a neutral, information-dense workbench domain such as field research, inventory, monitoring, or operations.
46
46
  - Use a continuous three-pane or similarly coherent workspace: navigation, queue/list, detail, metadata, and one signature observation band.
47
- - Keep color in the field, ribbon, markers, and state accents; keep text, controls, boundaries, and semantic hierarchy stable.
47
+ - Keep color in the field, map markers, and state accents. Keep the logo, avatar, rules, text, controls, boundaries, and semantic hierarchy neutral; do not paint them with palette gradients.
48
48
  - Prefer restrained surfaces and weak fills. Avoid turning every region into a floating card.
49
49
 
50
50
  ### 4. Implement the parameter contract
@@ -58,16 +58,18 @@ Maintain a serializable manifest with these top-level fields:
58
58
  base: { oklch },
59
59
  colors: [{
60
60
  id, label, oklch, srgbFallback,
61
- intensity, peakOpacity, lightnessBias,
61
+ intensity, peakOpacity,
62
62
  fieldScale, phase,
63
63
  measuredCoverage, effectiveShare
64
64
  }],
65
65
  field: { scale, octaves, warpStrength, motionSpeed, staticTime, ditherStrength, luminanceCap },
66
- output: { colorSpace, p3Enhancement, reducedMotion }
66
+ output: { colorSpace, reducedMotion }
67
67
  }
68
68
  ```
69
69
 
70
- - Keep every intensity in `[0, 1]`; make `overallColorIntensity` the global budget and `colors[].intensity` the per-color budget.
70
+ - Keep every intensity in `[0, 1]`; make `overallColorIntensity` the global budget and `colors[].intensity` the per-color budget. The renderer multiplies `intensity` by `peakOpacity`, so treat `peakOpacity` as the calibrated strength at full intensity.
71
+ - Order `colors` by hue. The renderer draws six hue stops around a loop and mixes each color with its array neighbors; near-complementary neighbors mix toward gray.
72
+ - `luminanceCap` applies only in `obsidian`. `ditherStrength` is the dither amplitude in output codes; `1` removes quantization bias.
71
73
  - Use OKLCH as the authoring space and provide an sRGB fallback for non-OKLCH contexts.
72
74
  - Keep the seed, static frame, phases, and field scales deterministic; do not use random per render.
73
75
  - Expose sliders for the global intensity and every configured color. Make reset, JSON export, and copy actions available.
@@ -77,8 +79,10 @@ Maintain a serializable manifest with these top-level fields:
77
79
  - Use a procedural fBm/domain-warp field or an equivalent continuous field; keep it behind the interface with `pointer-events: none`.
78
80
  - Use broad flowing hue regions or ribbons, not obvious radial blobs, spotlight circles, or hard rainbow bands.
79
81
  - Upload the complete palette and per-color field scales to the renderer. Apply the dark-mode luminance cap after palette mixing.
80
- - Provide a CSS fallback with comparable visual intent when WebGL is unavailable.
81
- - Pause or freeze motion when the document is hidden or `prefers-reduced-motion` is active.
82
+ - Encode output with the sRGB transfer function, dither in output codes, and quantize in the shader. A power-curve encode or a weak linear-light dither biases the output, most visibly in low-intensity fields and near black.
83
+ - Provide a CSS fallback with comparable visual intent when WebGL is unavailable, such as one soft hue-ordered sweep calibrated against the WebGL field; do not use fixed radial spots.
84
+ - Pause or freeze motion when the document is hidden or `prefers-reduced-motion` is active, and keep field time continuous so pausing and resuming do not jump.
85
+ - Repaint after every resize. Resizing clears the canvas, and a paused field has no next frame to redraw it.
82
86
  - Keep the renderer local and dependency-light; do not require remote fonts, images, or APIs for the starter.
83
87
 
84
88
  ### 6. Preserve interaction and accessibility
@@ -90,13 +94,10 @@ Maintain a serializable manifest with these top-level fields:
90
94
  ### 7. Validate and report
91
95
 
92
96
  - Scaffold a clean starter with `scripts/scaffold_template.py` when a neutral implementation is needed.
93
- - The bundled helpers parse only the restricted data-literal assignment used by
94
- the starter. They reject expressions, function calls, duplicate keys,
95
- unsupported syntax, trailing statements, and oversized manifests without
96
- executing JavaScript. Keep runtime theme configs data-only as well.
97
- - Run `scripts/validate_manifest.py` on each theme config before rendering.
98
- - Capture desktop and mobile screenshots with a real browser. Inspect them directly if visual capability is available.
99
- - Run `scripts/measure_preview.py` on the pure field screenshot and retain measured chromatic ratio, luminance statistics, per-color coverage, and effective share.
97
+ - Keep theme configs as data-only assignments. The bundled parser rejects expressions, function calls, duplicate keys, trailing statements, and oversized manifests without executing JavaScript.
98
+ - Run `scripts/validate_manifest.py` on each theme config before rendering, and resolve its warnings.
99
+ - Serve previews with `scripts/serve_preview.py`, which disables caching, then capture desktop and mobile screenshots with a real browser. Inspect them directly if visual capability is available.
100
+ - Run `scripts/measure_preview.py` on the pure field screenshot and retain measured chromatic ratio, tint, lightness shift, gray ratio, per-color coverage, and effective share.
100
101
  - Report configured parameters separately from measured values; do not imply that pixel attribution is an exact shader contribution.
101
102
  - Use `partial`, `visual-unverified`, or `blocked` when a required capability or native check is unavailable.
102
103
 
@@ -123,14 +124,17 @@ Use the bundled starter under `assets/starter/` as a neutral base. Copy only the
123
124
  ### scripts/
124
125
 
125
126
  - `scaffold_template.py`: copy the neutral starter for `opal`, `obsidian`, or both.
126
- - `manifest_parser.py`: statically parse the restricted data-only manifest grammar without executing JavaScript.
127
- - `validate_manifest.py`: validate required manifest fields and ranges.
128
- - `measure_preview.py`: measure a rendered pure-field PNG against the configured OKLCH palette.
127
+ - `manifest_parser.py`: statically parse the restricted data-only theme manifest without executing JavaScript.
128
+ - `validate_manifest.py`: validate required fields and ranges, and warn about hue order.
129
+ - `measure_preview.py`: measure a rendered pure-field PNG as OKLab deviation from the configured base, including lightness shift and gray ratio. Install optional dependencies from `scripts/requirements.txt` when needed.
130
+ - `serve_preview.py`: serve a directory locally with caching disabled.
131
+
132
+ Run `python -m unittest discover -s tests` after changing a script.
129
133
 
130
134
  ### references/
131
135
 
132
- - `style-contract.md`: mode-specific visual rules and recommended parameter ranges.
133
- - `verification.md`: visual-capability gate, browser QA, pixel measurement, and report schema.
136
+ - `style-contract.md`: mode-specific visual rules, recommended parameter ranges, and calibration for pages where no panel covers the field.
137
+ - `verification.md`: visual-capability gate, browser QA, pixel measurement, darkening check, and report schema.
134
138
 
135
139
  ### assets/
136
140
 
@@ -9,8 +9,8 @@ header span { display: block; margin-top: 16px; color: oklch(70% 0.01 220); font
9
9
  .template-list { border-top: 1px solid oklch(100% 0 0 / .12); }
10
10
  .template { display: grid; grid-template-columns: 116px minmax(0, 1fr) auto; align-items: center; gap: 24px; padding: 24px 0; border-bottom: 1px solid oklch(100% 0 0 / .12); color: inherit; text-decoration: none; }
11
11
  .swatch { width: 116px; height: 76px; border: 1px solid oklch(100% 0 0 / .15); border-radius: 10px; transition: transform 180ms cubic-bezier(.25,1,.5,1); }
12
- .opal .swatch { background: radial-gradient(circle at 20% 20%, oklch(90% .06 197), transparent 48%), radial-gradient(circle at 80% 35%, oklch(92% .055 18), transparent 48%), radial-gradient(circle at 46% 95%, oklch(90% .05 302), transparent 54%), oklch(98% .004 94); }
13
- .obsidian .swatch { background: radial-gradient(circle at 20% 20%, oklch(27% .07 188), transparent 50%), radial-gradient(circle at 80% 35%, oklch(23% .06 284), transparent 52%), radial-gradient(circle at 46% 95%, oklch(24% .05 78), transparent 56%), oklch(10% .012 236); }
12
+ .opal .swatch { background: linear-gradient(100deg, oklch(93.5% .047 18), oklch(94% .041 62), oklch(93.6% .036 159), oklch(92.8% .043 201), oklch(91.8% .05 248), oklch(92.5% .045 304)); }
13
+ .obsidian .swatch { background: linear-gradient(100deg, oklch(29% .055 188), oklch(25% .048 211), oklch(22.5% .047 251), oklch(24% .052 278), oklch(25.5% .05 318), oklch(30% .045 82)); }
14
14
  .copy small { display: block; color: oklch(58% 0.012 220); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
15
15
  .copy strong { display: block; margin-top: 5px; font-size: 18px; font-weight: 620; }
16
16
  .copy span { display: block; margin-top: 6px; color: oklch(68% 0.012 220); font-size: 12px; }
@@ -116,10 +116,11 @@
116
116
  <article class="pane detail-pane" aria-labelledby="record-title">
117
117
  <div class="pane-head">
118
118
  <div class="tab-list" role="tablist" aria-label="记录视图">
119
- <button class="tab" type="button" role="tab" aria-selected="true" aria-controls="detail-copy" data-copy="水线后退约 1.8 米,潮池外缘出现连续盐度跃迁。样本表面保持湿润,未观察到明显扰动。建议与 18:53 的浅滩网格记录并列复核。">记录</button>
120
- <button class="tab" type="button" role="tab" aria-selected="false" aria-controls="detail-copy" data-copy="关联了 4 张现场照片、2 个水质读数与 1 条位置修正。附件校验通过,原始文件已进入只读归档队列。">附件</button>
121
- <button class="tab" type="button" role="tab" aria-selected="false" aria-controls="detail-copy" data-copy="19:47 完成盐度标签修正;20:12 补充观察者备注;21:03 进入批次复核队列。所有变更均保留原始记录。">变更</button>
119
+ <button class="tab" type="button" role="tab" aria-controls="detail-copy" aria-selected="true" data-copy="水线后退约 1.8 米,潮池外缘出现连续盐度跃迁。样本表面保持湿润,未观察到明显扰动。建议与 18:53 的浅滩网格记录并列复核。">记录</button>
120
+ <button class="tab" type="button" role="tab" aria-controls="detail-copy" aria-selected="false" data-copy="关联了 4 张现场照片、2 个水质读数与 1 条位置修正。附件校验通过,原始文件已进入只读归档队列。">附件</button>
121
+ <button class="tab" type="button" role="tab" aria-controls="detail-copy" aria-selected="false" data-copy="19:47 完成盐度标签修正;20:12 补充观察者备注;21:03 进入批次复核队列。所有变更均保留原始记录。">变更</button>
122
122
  </div>
123
+ <button class="icon-button" type="button" aria-label="更多记录操作"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg></button>
123
124
  </div>
124
125
  <div class="detail-title">
125
126
  <h2 id="record-title">潮池边缘 · 样本 042</h2>
@@ -127,7 +128,7 @@
127
128
  </div>
128
129
  <div class="detail-body">
129
130
  <div class="field-notes">
130
- <p id="detail-copy" role="tabpanel">水线后退约 1.8 米,潮池外缘出现连续盐度跃迁。样本表面保持湿润,未观察到明显扰动。建议与 18:53 的浅滩网格记录并列复核。</p>
131
+ <p id="detail-copy" role="tabpanel">水线后退约 1.8 米,潮池外缘出现连续盐度跃迁。样本表面保持湿润,未观察到明显扰动。建议与 18:53 的浅滩网格记录并列复核。</p>
131
132
  <dl>
132
133
  <div class="note-rule"><dt>表面温度</dt><dd>18.6 °C · 稳定</dd></div>
133
134
  <div class="note-rule"><dt>盐度</dt><dd>31.8 PSU · 高于同批中位值 1.4</dd></div>
@@ -1,8 +1,8 @@
1
1
  window.SPECTRAL_THEME = {
2
- schemaVersion: "1.0",
2
+ schemaVersion: "1.1",
3
3
  mode: "obsidian",
4
4
  label: "五彩斑斓黑",
5
- preset: "obsidian-fieldnote-v1",
5
+ preset: "obsidian-fieldnote-v2",
6
6
  seed: 90821,
7
7
  overallColorIntensity: 1.0,
8
8
  base: {
@@ -16,7 +16,6 @@ window.SPECTRAL_THEME = {
16
16
  srgbFallback: "#183f3d",
17
17
  intensity: 0.72,
18
18
  peakOpacity: 0.29,
19
- lightnessBias: 0.008,
20
19
  fieldScale: 0.84,
21
20
  phase: [0.11, 0.72],
22
21
  measuredCoverage: null,
@@ -29,7 +28,6 @@ window.SPECTRAL_THEME = {
29
28
  srgbFallback: "#17333a",
30
29
  intensity: 0.62,
31
30
  peakOpacity: 0.27,
32
- lightnessBias: 0.006,
33
31
  fieldScale: 0.96,
34
32
  phase: [0.74, 0.19],
35
33
  measuredCoverage: null,
@@ -42,7 +40,6 @@ window.SPECTRAL_THEME = {
42
40
  srgbFallback: "#202b42",
43
41
  intensity: 0.54,
44
42
  peakOpacity: 0.26,
45
- lightnessBias: 0.004,
46
43
  fieldScale: 1.08,
47
44
  phase: [0.37, 0.88],
48
45
  measuredCoverage: null,
@@ -55,7 +52,6 @@ window.SPECTRAL_THEME = {
55
52
  srgbFallback: "#2d2948",
56
53
  intensity: 0.42,
57
54
  peakOpacity: 0.24,
58
- lightnessBias: 0.005,
59
55
  fieldScale: 0.91,
60
56
  phase: [0.93, 0.57],
61
57
  measuredCoverage: null,
@@ -68,7 +64,6 @@ window.SPECTRAL_THEME = {
68
64
  srgbFallback: "#3a293d",
69
65
  intensity: 0.30,
70
66
  peakOpacity: 0.21,
71
- lightnessBias: 0.004,
72
67
  fieldScale: 1.14,
73
68
  phase: [0.25, 0.31],
74
69
  measuredCoverage: null,
@@ -81,7 +76,6 @@ window.SPECTRAL_THEME = {
81
76
  srgbFallback: "#473c22",
82
77
  intensity: 0.18,
83
78
  peakOpacity: 0.17,
84
- lightnessBias: 0.007,
85
79
  fieldScale: 1.03,
86
80
  phase: [0.61, 0.46],
87
81
  measuredCoverage: null,
@@ -94,12 +88,11 @@ window.SPECTRAL_THEME = {
94
88
  warpStrength: 0.41,
95
89
  motionSpeed: 0.01,
96
90
  staticTime: 2.18,
97
- ditherStrength: 0.82,
91
+ ditherStrength: 1,
98
92
  luminanceCap: 0.165
99
93
  },
100
94
  output: {
101
95
  colorSpace: "srgb",
102
- p3Enhancement: "feature-detected",
103
96
  reducedMotion: "frozen-calibrated-frame"
104
97
  }
105
98
  };