opencode-skills-collection 4.1.2 → 4.1.3
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 +9 -1
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +15 -1
- package/bundled-skills/deep-research-framework/SKILL.md +81 -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/tech-writing-proofread/SKILL.md +78 -0
- package/bundled-skills/typescript-expert/scripts/ts_diagnostic.py +40 -19
- package/bundled-skills/youtube-summarizer/SKILL.md +3 -3
- package/package.json +1 -1
- package/skills_index.json +254 -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,
|
|
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,
|
|
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,
|
|
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
|
-
-
|
|
81
|
-
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
127
|
-
- `validate_manifest.py`: validate required
|
|
128
|
-
- `measure_preview.py`: measure a rendered pure-field PNG
|
|
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
|
|
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:
|
|
13
|
-
.obsidian .swatch { background:
|
|
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-
|
|
120
|
-
<button class="tab" type="button" role="tab" aria-
|
|
121
|
-
<button class="tab" type="button" role="tab" aria-
|
|
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
|
-
|
|
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.
|
|
2
|
+
schemaVersion: "1.1",
|
|
3
3
|
mode: "obsidian",
|
|
4
4
|
label: "五彩斑斓黑",
|
|
5
|
-
preset: "obsidian-fieldnote-
|
|
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:
|
|
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
|
};
|