Repository navigation
239 lines (213 loc) · 10.3 KB
/
Copy pathci.yml
File metadata and controls
239 lines (213 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx tsc --noEmit
# Inter is fetched from a GitHub release. Cache it so CI is not one rate
# limit away from red, and so reruns do not re-download 30 MB.
- name: Cache Inter
id: fonts
uses: actions/cache@v4
with:
path: engine/assets/fonts
key: inter-4.1-${{ hashFiles('engine/src/fonts.ts') }}
- name: Cache Playwright browsers
id: browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install fonts + Chromium
run: npm run setup
# A cache miss plus a failed download would otherwise surface much later as
# a confusing render error, so fail here with the real reason.
- name: Fonts are present
run: |
for f in Inter-Regular Inter-SemiBold Inter-Bold Inter-ExtraBold; do
test -s "engine/assets/fonts/$f.woff2" || {
echo "::error::engine/assets/fonts/$f.woff2 missing after setup"
exit 1
}
done
echo "all four Inter faces present"
# Every format.json must validate, every meta.yml must declare where its
# example numbers came from, and every content axis must be declared.
- name: Validate the format library
run: npx kw check
# The first frame is the hook AND the cover image TikTok/Instagram show
# before playback. It has broken twice for unrelated reasons, both times
# because the contract was only eyeballed on one format. This asserts, for
# ALL formats, that frame 0 paints its promised elements and contains real
# ink — DOM state and pixels checked independently.
- name: Frame 0 is never blank, in every format
run: npx kw frame0
# One real render with the determinism probe: frames are re-seeked out of
# order and compared by hash. This is the test that protects every
# measurement in the repo.
- name: Render with determinism check
run: npx kw render stat-counter-rise --check-determinism
# The previews are committed, so this needs no rendering — it recomputes
# each format's variant id and compares it with the manifest. A spec that
# changed without its preview being re-rendered fails here, which is the
# only thing standing between an edited format and a stale clip on the
# published gallery.
- name: Committed previews match their specs
run: npx kw previews check
- name: Gallery builds
run: npx kw site build
# The social card is built from four committed preview frames, so it drifts
# the same way a preview does: change a featured format and the shared link
# still advertises the old clip. Needs no browser — it recomputes the
# variant ids and compares them with the card's manifest.
- name: Social card matches the formats it shows
run: npx kw social check
# The page's equivalent of the frame tests. Opens the built gallery in a
# real browser at phone, tablet and desktop widths and asserts every
# control and card row is on screen and actually clickable. A filter chip
# that slides under the sort controls hides a whole format family while
# looking fine in a screenshot, which is exactly how this shipped.
- name: Gallery layout is reachable at every width
run: npx kw layout
- name: The generated format index is committed and current
run: git diff --exit-code skills/keepwatching/references/formats.md
# The reader that turns an analytics export into numbers, tested under four
# timezones.
#
# This is not thoroughness for its own sake. The one bug that has actually
# shipped from this repo was a date read through `new Date(...).toISOString()`,
# which lands on the right day in UTC and a day early almost everywhere else.
# A suite that only ever runs in UTC is a suite that would have gone green on
# it — verified by putting the bug back: 35/35 passing under TZ=UTC, three
# failures under Europe/Istanbul. UTC alone proves nothing about dates.
parsing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
# One checkout, four runs: the suite takes under a second, so a matrix of
# four jobs would spend all its time on npm ci.
- name: Unit tests, in every timezone the numbers might be read in
run: |
for tz in UTC Europe/Istanbul Pacific/Kiritimati America/Los_Angeles; do
echo "::group::TZ=$tz"
TZ=$tz npm test || { echo "::error::unit tests failed under TZ=$tz"; exit 1; }
echo "::endgroup::"
done
measurement-hygiene:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
# The honesty rules, enforced. A data.yml that claims numbers must carry a
# sample size and list its samples — and a content-axis number must never
# appear inside the format block, which is what a merged schema would look
# like after a bad hand-edit.
- name: Evidence blocks are well formed and separate
run: |
node -e '
const fs = require("fs"), path = require("path"), YAML = require("yaml");
const axes = new Set(
(YAML.parse(fs.readFileSync("data/content-axes.yml", "utf8")).axes || [])
.map(a => a.id)
);
let bad = 0;
const fail = m => { console.error(`::error::${m}`); bad++; };
for (const slug of fs.readdirSync("formats")) {
const f = path.join("formats", slug, "data.yml");
if (!fs.existsSync(f)) continue;
const d = YAML.parse(fs.readFileSync(f, "utf8")) || {};
if (!d.format || !d.content_axis) {
fail(`${slug}: data.yml must have both a format and a content_axis block`);
continue;
}
if ("axes" in d.format) fail(`${slug}: axis data found inside the format block`);
if ("avgViewedPct" in d.content_axis) {
fail(`${slug}: content_axis must not carry a top-level format metric`);
}
// `not-in-library` marks a published video whose format is
// deliberately outside this repo. It is a content-axis sample and
// never a format sample, so it must not appear anywhere in a
// format directory.
const text = fs.readFileSync(f, "utf8");
if (text.includes("not-in-library")) {
fail(`${slug}: a not-in-library sample reached a format data.yml`);
}
const fm = d.format;
const claims = fm.avgViewedPct != null || fm.hook3s != null;
if (claims && !(fm.n > 0)) fail(`${slug}: format has numbers but n is ${fm.n}`);
if (fm.status === "measured" && !(fm.samples && fm.samples.length)) {
fail(`${slug}: format status is measured but no samples are listed`);
}
const ca = d.content_axis;
const total = (ca.axes || []).reduce((s, a) => s + (a.n || 0), 0);
if ((ca.n || 0) !== total) fail(`${slug}: content_axis n (${ca.n}) != sum of axes (${total})`);
for (const a of ca.axes || []) {
if (!axes.has(a.axis)) fail(`${slug}: unknown content axis "${a.axis}"`);
if (a.avgViewedPct != null && !(a.n > 0)) fail(`${slug}: axis ${a.axis} has numbers but n is ${a.n}`);
}
}
// The repo-level axis roll-up must not smuggle format results in.
if (fs.existsSync("data/content-axis-results.yml")) {
const ar = YAML.parse(fs.readFileSync("data/content-axis-results.yml", "utf8")) || {};
const declared = axes;
for (const a of ar.axes || []) {
if (!declared.has(a.axis)) fail(`axis results: unknown axis "${a.axis}"`);
if (!Array.isArray(a.carriedBy) || a.carriedBy.length === 0) {
fail(`axis results: "${a.axis}" has no carriedBy — the confound must be visible`);
}
if (a.avgViewedPct != null && !(a.n > 0)) {
fail(`axis results: "${a.axis}" has numbers but n is ${a.n}`);
}
}
}
if (bad) process.exit(1);
console.log("evidence hygiene ok");
'
# A library about honest measurement must not ship unattributed numbers in
# its own demo copy.
- name: Every format declares where its example numbers came from
run: |
node -e '
const fs = require("fs"), path = require("path"), YAML = require("yaml");
let bad = 0;
for (const slug of fs.readdirSync("formats")) {
const m = YAML.parse(fs.readFileSync(path.join("formats", slug, "meta.yml"), "utf8")) || {};
if (m.sampleContent === "sourced") {
if (!(m.sources || []).length) {
console.error(`::error::${slug}: sourced but no sources listed`); bad++;
}
for (const s of m.sources || []) {
if (!/^https?:\/\//.test(s.url || "")) {
console.error(`::error::${slug}: source needs a http(s) url`); bad++;
}
if (!s.claim) {
console.error(`::error::${slug}: source needs the claim it backs`); bad++;
}
}
} else if (m.sampleContent !== "placeholder") {
console.error(`::error::${slug}: meta.yml must declare sampleContent: sourced | placeholder`);
bad++;
}
}
if (bad) process.exit(1);
console.log("sample-content provenance ok");
'