quiver-cli 1.3.0 → 1.3.1
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/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
- package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
- package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
- package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
- package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
- package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
- package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
- package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
- package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
- package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
- package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
- package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
- package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
- package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
- package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
- package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
- package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
- package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
- package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
- package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
- package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
- package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
- package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
- package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
- package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
- package/template/.agents/skills/hono/SKILL.md +17 -3
- package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
- package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
- package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
- package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
- package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
- package/template/.agents/skills/supabase/SKILL.md +5 -1
- package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
- package/template/.agents/skills/writing/humanizer/README.md +209 -0
- package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
- package/template/.agents/upstreams.json +21 -22
- package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
|
@@ -31,6 +31,16 @@
|
|
|
31
31
|
* recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a
|
|
32
32
|
* fresh assigned index, challengers, and compositions. One base key therefore
|
|
33
33
|
* reproduces the entire chain of rounds.
|
|
34
|
+
* - REGISTER (--register safer|bolder): the user's steering on the
|
|
35
|
+
* familiar-to-bold axis, applied to a re-roll round. A register changes
|
|
36
|
+
* only what this round instructs, never what it dealt: the same key and
|
|
37
|
+
* reroll count reproduce the same deal whatever the register, so the
|
|
38
|
+
* exclusion chain never forks. bolder presents the dealt foreign forms
|
|
39
|
+
* as the whole hand (first-dealt leads, dice-assigned by deal order);
|
|
40
|
+
* safer spends the dealt hand unseen and presents the familiar register,
|
|
41
|
+
* the model's conventional grounded candidates plus the canon against
|
|
42
|
+
* named competitors, the one sanctioned lineup of the model's own list.
|
|
43
|
+
* Registers are user-requested, never pre-selected by the model.
|
|
34
44
|
* - RATINGS: the reviewer's approval ratings weight the challenger draw
|
|
35
45
|
* (3-star doubles the odds, 1-star sits out); the approved pool itself
|
|
36
46
|
* is unchanged.
|
|
@@ -41,7 +51,9 @@
|
|
|
41
51
|
* node scripts/concept-seed.mjs --scope surface --mode operate --grain flow
|
|
42
52
|
* node scripts/concept-seed.mjs --scope direction --candidate-count 6
|
|
43
53
|
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1
|
|
44
|
-
* node scripts/concept-seed.mjs --
|
|
54
|
+
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1 --register bolder
|
|
55
|
+
* node scripts/concept-seed.mjs --chosen <challenger-id> --kind challenger --from <key> --scope direction
|
|
56
|
+
* node scripts/concept-seed.mjs --kind assigned --from <key> --scope direction
|
|
45
57
|
*
|
|
46
58
|
* --grain names how much of the product is in play: product, flow, view, or
|
|
47
59
|
* region. A docs site, an onboarding flow, a landing page and a data table are
|
|
@@ -62,8 +74,13 @@
|
|
|
62
74
|
* Challenger data resolves in order: a local catalog directory (the private
|
|
63
75
|
* service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll
|
|
64
76
|
* API at impeccable.style, then a degraded assignment-only seed when both are
|
|
65
|
-
* unavailable.
|
|
66
|
-
*
|
|
77
|
+
* unavailable. The anonymous choice ping fires once per resolved attended
|
|
78
|
+
* round on API-dealt rolls: --kind names which card class won (assigned,
|
|
79
|
+
* pick, challenger, canon) so share metrics have a denominator, --chosen
|
|
80
|
+
* carries the catalog id when a dealt challenger won, and --register rides
|
|
81
|
+
* along when the round came from a steered hand. Grounded candidates' names
|
|
82
|
+
* never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables
|
|
83
|
+
* the ping entirely.
|
|
67
84
|
*
|
|
68
85
|
* Env vars:
|
|
69
86
|
* IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs.
|
|
@@ -73,7 +90,8 @@
|
|
|
73
90
|
*/
|
|
74
91
|
|
|
75
92
|
import crypto from 'node:crypto';
|
|
76
|
-
import { dirname, join, resolve } from 'node:path';
|
|
93
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
94
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
77
95
|
import { fileURLToPath } from 'node:url';
|
|
78
96
|
import {
|
|
79
97
|
approvedPoolRevision,
|
|
@@ -172,17 +190,35 @@ function telemetryDisabled() {
|
|
|
172
190
|
return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK);
|
|
173
191
|
}
|
|
174
192
|
|
|
175
|
-
// Anonymous choice ping:
|
|
193
|
+
// Anonymous choice ping: one per resolved attended direction round. kind
|
|
194
|
+
// says which card class won (assigned / pick / challenger / canon), so
|
|
195
|
+
// pick-share and canon-share have a denominator; chosenId rides along only
|
|
196
|
+
// when a dealt catalog world won, and register only when the round came from
|
|
197
|
+
// a steered hand. Grounded candidates' names never leave the machine: they
|
|
198
|
+
// are derived from the user's project, so the ping carries the kind alone.
|
|
176
199
|
// Fire-and-forget; never fails the caller.
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']);
|
|
201
|
+
export async function pingChosen({ chosenId, key, scope, mode, kind, register }) {
|
|
202
|
+
if (telemetryDisabled()) return false;
|
|
203
|
+
if (kind && !PING_KINDS.has(kind)) return false;
|
|
204
|
+
if (register && register !== 'safer' && register !== 'bolder') return false;
|
|
205
|
+
// Legacy shape: a bare challenger id with no kind stays a valid ping.
|
|
206
|
+
if (!chosenId && !kind) return false;
|
|
207
|
+
if ((kind === 'challenger' || !kind) && !chosenId) return false;
|
|
179
208
|
const controller = new AbortController();
|
|
180
209
|
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
|
|
181
210
|
try {
|
|
182
211
|
await fetch(`${API_BASE}/chosen`, {
|
|
183
212
|
method: 'POST',
|
|
184
213
|
headers: { 'Content-Type': 'application/json' },
|
|
185
|
-
body: JSON.stringify({
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
...(chosenId ? { chosenId } : {}),
|
|
216
|
+
key,
|
|
217
|
+
scope,
|
|
218
|
+
mode,
|
|
219
|
+
...(kind ? { kind } : {}),
|
|
220
|
+
...(register ? { register } : {}),
|
|
221
|
+
}),
|
|
186
222
|
signal: controller.signal,
|
|
187
223
|
});
|
|
188
224
|
return true;
|
|
@@ -260,6 +296,7 @@ export function renderConceptSeed({
|
|
|
260
296
|
scope = 'surface',
|
|
261
297
|
key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
|
|
262
298
|
reroll = 0,
|
|
299
|
+
register = null,
|
|
263
300
|
mode = null,
|
|
264
301
|
grain = null,
|
|
265
302
|
platform = null,
|
|
@@ -273,6 +310,15 @@ export function renderConceptSeed({
|
|
|
273
310
|
if (!Number.isInteger(reroll) || reroll < 0) {
|
|
274
311
|
throw new Error('concept-seed: --reroll must be a non-negative integer');
|
|
275
312
|
}
|
|
313
|
+
if (register !== null && register !== 'safer' && register !== 'bolder') {
|
|
314
|
+
throw new Error('concept-seed: --register must be safer or bolder');
|
|
315
|
+
}
|
|
316
|
+
if (register !== null && reroll < 1) {
|
|
317
|
+
throw new Error('concept-seed: --register steers a re-roll round; pass --reroll <n> with it');
|
|
318
|
+
}
|
|
319
|
+
if (register !== null && scope !== 'direction') {
|
|
320
|
+
throw new Error('concept-seed: --register applies to direction rounds only');
|
|
321
|
+
}
|
|
276
322
|
if (mode !== null && !SEED_MODES.has(mode)) {
|
|
277
323
|
throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
|
|
278
324
|
}
|
|
@@ -293,6 +339,20 @@ export function renderConceptSeed({
|
|
|
293
339
|
};
|
|
294
340
|
const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
|
|
295
341
|
const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
|
|
342
|
+
// Surface scope deals a hand of three grounded structures: one card is not
|
|
343
|
+
// a choice, and the full ranked list would hand selection back to the
|
|
344
|
+
// model's taste. The dice pick all three; the primary index leads. The
|
|
345
|
+
// no-lineup rule stays direction-only, where it was written for worlds.
|
|
346
|
+
const dealtIndices = [buildIndex];
|
|
347
|
+
for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) {
|
|
348
|
+
const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount);
|
|
349
|
+
if (!dealtIndices.includes(idx)) dealtIndices.push(idx);
|
|
350
|
+
if (draw > 64) { // hash repeats cannot stall the deal
|
|
351
|
+
for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) {
|
|
352
|
+
if (!dealtIndices.includes(fill)) dealtIndices.push(fill);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
296
356
|
|
|
297
357
|
// Local catalog first (private repo, evals, tests), then the roll API,
|
|
298
358
|
// then a degraded assignment-only seed. The assigned index is pure local
|
|
@@ -326,6 +386,7 @@ export function renderConceptSeed({
|
|
|
326
386
|
scope,
|
|
327
387
|
key,
|
|
328
388
|
reroll,
|
|
389
|
+
register,
|
|
329
390
|
mode,
|
|
330
391
|
grain,
|
|
331
392
|
platform,
|
|
@@ -357,16 +418,32 @@ export function renderConceptSeed({
|
|
|
357
418
|
survive the current task plus navigation, quiet and dense content,
|
|
358
419
|
interaction and state, and a substantially different future surface. In an
|
|
359
420
|
attended run, present the assigned direction fully committed and offer
|
|
360
|
-
re-roll
|
|
421
|
+
re-roll. You may add ONE card for your top-ranked grounded candidate when
|
|
422
|
+
it is not the assigned direction, kicker IMPECCABLE’S PICK, with an honest risk line
|
|
423
|
+
naming its familiarity; one pick card, never a ranked lineup, and the pick
|
|
424
|
+
never takes the lead position. When the assignment IS your top candidate,
|
|
425
|
+
there is no pick card. Re-roll yourself only
|
|
361
426
|
on named factual grounds, when the assignment cannot carry the product's
|
|
362
427
|
truth or task; taste is never grounds.`
|
|
363
428
|
: `After ordering the task's grounded structural candidates by resonance,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
429
|
+
deal candidates ${dealtIndices.join(', ')} of your own grounded list to the
|
|
430
|
+
table; index ${buildIndex} leads, and the deal never points at a challenger.
|
|
431
|
+
The deal is the roll, not a suggestion: the dice decide which structures
|
|
432
|
+
reach the user, so the ranking rut stays broken while the user still gets a
|
|
433
|
+
real choice, and the full ranked list stays yours. In an attended run,
|
|
434
|
+
present the three dealt structures as full cards of equal salience, the
|
|
435
|
+
lead carrying kicker THE ROLL, with steer and re-roll, and let the user
|
|
436
|
+
lock one in; the world is already settled, so this choice is composition.
|
|
437
|
+
Visualize every dealt card: with image generation available and a
|
|
438
|
+
comp-led default (.impeccable/config.json buildPath; the page toggle
|
|
439
|
+
handles the exception), declare a comp per card and generate after
|
|
440
|
+
serving, lead first; otherwise author each card's wireframe field (see
|
|
441
|
+
serve-question --schema) and the page draws the schematic. Carry the
|
|
442
|
+
recorded default in the payload as buildPath with toggle: true. Locking a card
|
|
443
|
+
approves its comp: a surface round that put three visualized structures on
|
|
444
|
+
the table replaces the three-option comp round in visualize.md. Re-roll
|
|
445
|
+
yourself only when every dealt structure fails audience identification or
|
|
446
|
+
product clarity on named factual grounds.`;
|
|
370
447
|
|
|
371
448
|
const challengerInstruction = scope === 'direction'
|
|
372
449
|
? `Fuse each challenger before judging it: the challenger supplies the form
|
|
@@ -374,7 +451,16 @@ export function renderConceptSeed({
|
|
|
374
451
|
conflicts. Weigh the fused result against the assigned direction on exactly
|
|
375
452
|
two axes, audience identification and product clarity. Losing to strong
|
|
376
453
|
grounded material is a valid outcome; beating a thin or tool-monoculture
|
|
377
|
-
list is the point. A fused challenger that wins both axes becomes the build
|
|
454
|
+
list is the point. A fused challenger that wins both axes becomes the build.
|
|
455
|
+
Close the weighing with a verdict per challenger, decided before any
|
|
456
|
+
borrowing is considered: wins (beats the assigned direction on both axes),
|
|
457
|
+
competitive (holds one axis), or declined (loses both). A declined
|
|
458
|
+
challenger is not spent: name the one discipline of its system the assigned
|
|
459
|
+
direction lacks, and raise the assigned direction to match before
|
|
460
|
+
presenting it. A donation transfers ambition and system discipline, never
|
|
461
|
+
the challenger's clothes; one world owns the page. Write each raise as its
|
|
462
|
+
own named line on the presented direction, and carry every verdict, kept
|
|
463
|
+
line, and raise into the decision page payload.`
|
|
378
464
|
: `A challenger wins only when its fused result beats the grounded list on
|
|
379
465
|
audience identification and product clarity. It may change task topology or
|
|
380
466
|
interaction, but never the committed visual identity.`;
|
|
@@ -399,8 +485,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens
|
|
|
399
485
|
the product without weakening semantics, performance, or fallback behavior.`;
|
|
400
486
|
|
|
401
487
|
if (!data) {
|
|
402
|
-
|
|
403
|
-
|
|
488
|
+
// A degraded roll can still serve the safer register, which needs no
|
|
489
|
+
// catalog at all: the assignment machinery is suppressed entirely, the
|
|
490
|
+
// same as the non-degraded safer round, because emitting both "the user
|
|
491
|
+
// picks" and a mandatory numbered build order hands the model two
|
|
492
|
+
// contradicting instructions and the mandatory one tends to win. The
|
|
493
|
+
// bolder register is exactly the thing degradation took away, so it
|
|
494
|
+
// falls back to a plain grounded round, disclosed.
|
|
495
|
+
const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`;
|
|
496
|
+
if (register === 'safer') {
|
|
497
|
+
return `${degradedHeader}
|
|
498
|
+
SAFER REGISTER (user-requested): the assigned index is suspended this
|
|
499
|
+
round; the user picks, and no candidate is mandated. Present the familiar
|
|
500
|
+
register: your remaining grounded candidates from the conventional end, at
|
|
501
|
+
most three, as full cards with an honest risk line each, plus the canon
|
|
502
|
+
executed against two or three named competitors. This is the one sanctioned
|
|
503
|
+
lineup of your own ranked candidates; it exists only by this explicit
|
|
504
|
+
request. When the user voices a standing preference for it, record a brand
|
|
505
|
+
commitment in PRODUCT.md.
|
|
506
|
+
${authorityInstruction}
|
|
507
|
+
A user- or brief-pinned decision beats the roll, always.
|
|
508
|
+
REGISTER (restated for truncated readers): safer, user-requested; the
|
|
509
|
+
assigned index is suspended this round and the user picks; seed key ${key}.
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
const degradedRegister = register === 'bolder'
|
|
513
|
+
? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran
|
|
514
|
+
degraded with no catalog and no roll service, so there is nothing bold to
|
|
515
|
+
deal. Tell the user, then run this round as a plain grounded re-roll; the
|
|
516
|
+
assignment below applies.
|
|
517
|
+
`
|
|
518
|
+
: '';
|
|
519
|
+
return `${degradedHeader}
|
|
520
|
+
${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
|
|
404
521
|
${promotedInstruction}
|
|
405
522
|
The assignment exists to refuse the model's ranking rut, never to outrank
|
|
406
523
|
the user or the brief. Never expose assignment metadata in user-facing labels.
|
|
@@ -424,8 +541,11 @@ channel: when a browser can open, present the direction on the decision page
|
|
|
424
541
|
the no-browser fallback.
|
|
425
542
|
${authorityInstruction}
|
|
426
543
|
A user- or brief-pinned decision beats the roll, always.
|
|
427
|
-
|
|
428
|
-
|
|
544
|
+
${scope === 'direction'
|
|
545
|
+
? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
|
|
546
|
+
${buildIndex} of your own grounded list; seed key ${key}.`
|
|
547
|
+
: `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
|
|
548
|
+
${buildIndex} leads. Present all three dealt structures; seed key ${key}.`}
|
|
429
549
|
`;
|
|
430
550
|
}
|
|
431
551
|
|
|
@@ -471,58 +591,186 @@ structure only, never a palette, typeface, or material. Treat them as serious
|
|
|
471
591
|
rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n`
|
|
472
592
|
: '';
|
|
473
593
|
const rerollBlock = reroll > 0
|
|
474
|
-
? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded
|
|
475
|
-
and challenger alike, is eliminated and may not return reworded
|
|
594
|
+
? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded
|
|
595
|
+
and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive
|
|
476
596
|
genuinely new grounded candidates from unexplored angles before judging
|
|
477
|
-
these fresh challengers
|
|
597
|
+
these fresh challengers.`}\n`
|
|
478
598
|
: '';
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
599
|
+
// A register swaps the round's presentation, never its deal: the assigned
|
|
600
|
+
// index and challenger fetch stay identical so the chain reproduces, and
|
|
601
|
+
// only the instructions change.
|
|
602
|
+
const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this
|
|
603
|
+
round's dealt hand is spent unseen, stays excluded from future rounds, and
|
|
604
|
+
is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded
|
|
605
|
+
candidates from the conventional end, at most three, as full cards with an
|
|
606
|
+
honest risk line each, plus the canon executed against two or three named
|
|
607
|
+
competitors. This is the one sanctioned lineup of your own ranked
|
|
608
|
+
candidates; it exists only by this explicit request. When the user voices a
|
|
609
|
+
standing preference for it, record a brand commitment in PRODUCT.md.`;
|
|
610
|
+
const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no
|
|
611
|
+
grounded direction is presented this round and the assigned index is
|
|
612
|
+
suspended. The hand is every dealt challenger below, each fused with the
|
|
613
|
+
product and presented as a full card; the FIRST dealt challenger leads, an
|
|
614
|
+
assignment by deal order, so the dice still choose. Verdicts and donations
|
|
615
|
+
apply between the challengers, weighed against the leader. The pick card
|
|
616
|
+
sits out; the canon stays, as always.`;
|
|
617
|
+
// The one command that follows a resolved choice. It records the choice
|
|
618
|
+
// (anonymous telemetry on API-dealt rolls; skipped under DO_NOT_TRACK /
|
|
619
|
+
// IMPECCABLE_NO_TELEMETRY) and opens the build's phase machine, whose
|
|
620
|
+
// first gate is the comp round on a comp-led build. Every run that skipped
|
|
621
|
+
// the comp round did so by treating a separate "telemetry ping" as
|
|
622
|
+
// bookkeeping: suppressed with >/dev/null, run after the page was written,
|
|
623
|
+
// or never run. So there is no separate ping; the start command is the
|
|
624
|
+
// ping, and it is not optional.
|
|
625
|
+
const nextCommand = scope === 'direction'
|
|
626
|
+
? `AFTER THE CHOICE, run exactly one command and follow what it prints (do not suppress its output; do not write page code before it):
|
|
627
|
+
node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --direction ${key} --kind <assigned|pick|challenger|canon>${data.source === 'api' ? ' [--chosen <challenger-id>]' : ''}${register ? ` --register ${register}` : ''}
|
|
628
|
+
It records the choice${data.source === 'api' ? ' (anonymous: card kind plus catalog id; skipped under DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY)' : ''} and opens the build phases: on a comp-led build the comp round is the first gate (three comps, one approved) and no page code is written before it closes; on a code-led build it prints the contract step. A build without this state file is a build the finish reviewer treats as having skipped the round.\n`
|
|
629
|
+
: (data.source === 'api'
|
|
630
|
+
? `AFTER THE CHOICE, run once: node ${relative(process.cwd(), here) || '.'}/concept-seed.mjs --kind <assigned|pick|challenger|canon> --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} (records the choice; the locked card's comp is the approved comp, so then: node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp <that comp>).\n`
|
|
631
|
+
: `AFTER THE CHOICE: the locked card's comp is the approved comp; run node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp <that comp> and follow what it prints.\n`);
|
|
632
|
+
const telemetryBlock = nextCommand;
|
|
633
|
+
const assignedBlock = register === null
|
|
634
|
+
? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
|
|
487
635
|
${promotedInstruction}
|
|
488
636
|
The assignment exists to refuse the model's ranking rut, never to outrank
|
|
489
|
-
the user or the brief. Never expose assignment metadata in user-facing labels
|
|
490
|
-
|
|
637
|
+
the user or the brief. Never expose assignment metadata in user-facing labels.`
|
|
638
|
+
: register === 'safer' ? saferBlock : bolderBlock;
|
|
639
|
+
// A bolder round has no assigned grounded direction, so the generic
|
|
640
|
+
// weighing instruction (which measures against the assignment) would
|
|
641
|
+
// contradict the register; the bolder variant weighs against the leader.
|
|
642
|
+
const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form
|
|
643
|
+
and its system grammar, the product supplies every fact, and clarity wins
|
|
644
|
+
conflicts. Weigh every fused challenger against the fused LEADER, the first
|
|
645
|
+
dealt, on exactly two axes, audience identification and product clarity;
|
|
646
|
+
verdicts and donations apply between the challengers, and one that beats
|
|
647
|
+
the leader on both axes presents as the hand's strongest alternate.`;
|
|
648
|
+
const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction;
|
|
649
|
+
const challengerSection = register === 'safer'
|
|
650
|
+
? ''
|
|
651
|
+
: `CHALLENGERS:
|
|
491
652
|
${data.challengers.map(renderChallenger).join('\n')}
|
|
492
|
-
${compositionBlock}${
|
|
653
|
+
${compositionBlock}${roundChallengerInstruction}
|
|
493
654
|
When you can view images, open the QUALITY BAR board and hero for any
|
|
494
655
|
challenger you weigh seriously and for the world you build. They exist as a
|
|
495
656
|
craft bar, the finish level and commitment the build is expected to reach,
|
|
496
657
|
never as a mockup to copy; your surface serves this product, not that render.
|
|
497
|
-
|
|
658
|
+
`;
|
|
659
|
+
const restated = register === null
|
|
660
|
+
? (scope === 'direction'
|
|
661
|
+
? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
|
|
662
|
+
${buildIndex} of your own grounded list; seed key ${key}.`
|
|
663
|
+
: `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
|
|
664
|
+
${buildIndex} leads. Present all three dealt structures; seed key ${key}.`)
|
|
665
|
+
: `REGISTER (restated for truncated readers): ${register}, user-requested; the
|
|
666
|
+
assigned index is suspended this round; seed key ${key}.`;
|
|
667
|
+
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
|
|
668
|
+
${rerollBlock}${assignedBlock}
|
|
669
|
+
${challengerSection}${authorityInstruction}
|
|
498
670
|
${richnessInstruction}
|
|
499
671
|
${telemetryBlock}A user- or brief-pinned decision beats the roll, always.
|
|
500
|
-
|
|
501
|
-
${buildIndex} of your own grounded list; seed key ${key}.
|
|
672
|
+
${restated}
|
|
502
673
|
`;
|
|
503
674
|
}
|
|
504
675
|
|
|
505
|
-
|
|
676
|
+
/**
|
|
677
|
+
* What the model must do next, once a direction (or surface structure) is
|
|
678
|
+
* chosen. Read from the same config the boot directive reads:
|
|
679
|
+
* `.impeccable/config.local.json` over `.impeccable/config.json`,
|
|
680
|
+
* `buildPath` comp|code; with neither, comp-led whenever image generation
|
|
681
|
+
* exists (an OpenAI key here; a harness-native image tool is invisible to
|
|
682
|
+
* this script, so the text names it too), code-led otherwise.
|
|
683
|
+
*/
|
|
684
|
+
export function nextStepAfterChoice({ key, scope, cwd = process.cwd(), env = process.env } = {}) {
|
|
685
|
+
let buildPath = null;
|
|
686
|
+
for (const name of ['config.json', 'config.local.json']) {
|
|
687
|
+
try {
|
|
688
|
+
const raw = JSON.parse(readFileSync(resolve(cwd, '.impeccable', name), 'utf8'));
|
|
689
|
+
if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') buildPath = raw.buildPath;
|
|
690
|
+
} catch { /* absent */ }
|
|
691
|
+
}
|
|
692
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
693
|
+
const scripts = relative(cwd, scriptsDir) || '.';
|
|
694
|
+
const imageGen = !!env.OPENAI_API_KEY;
|
|
695
|
+
const seed = key ? ` --direction ${key}` : '';
|
|
696
|
+
if (buildPath === 'code') {
|
|
697
|
+
return `NEXT (code-led, from .impeccable config): write the direction contract, then build; no comp round. Load reference/new-work.md section 5 and 6.\n`;
|
|
698
|
+
}
|
|
699
|
+
const why = buildPath === 'comp' ? 'from .impeccable config' : imageGen ? 'default: image generation is available' : 'default: comp-led unless no image tool exists; if your harness truly has none and there is no OpenAI key, this is code-led and you say so in one line';
|
|
700
|
+
if (scope === 'surface') {
|
|
701
|
+
return `NEXT (comp-led, ${why}): the locked card's comp is the approved comp. Run: node ${scripts}/build-phase.mjs start --comp <that comp> and follow its NEXT lines. Do not write page code before build-phase.mjs advance has closed the spec, plates, and hero gates.\n`;
|
|
702
|
+
}
|
|
703
|
+
return `NEXT (comp-led, ${why}): the world is chosen; the composition is not. Run: node ${scripts}/build-phase.mjs start${seed} and follow its NEXT lines: it opens the comps phase (three comps under .impeccable/mocks/, one approved by the user through the decision page or structured question, sidecar "approved": true), then spec, plates, hero, sections, motion, responsive, review. Do not write page code before those gates close. Reference: reference/visualize.md for the comp round.\n`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function sameMainModulePath(left, right, platform = process.platform) {
|
|
707
|
+
if (platform !== 'win32') return left === right;
|
|
708
|
+
const normalizeDriveLetter = (value) => value.replace(/^([a-z]):/i, (_, drive) => `${drive.toUpperCase()}:`);
|
|
709
|
+
return normalizeDriveLetter(left) === normalizeDriveLetter(right);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function isMainModule() {
|
|
713
|
+
if (!process.argv[1]) return false;
|
|
714
|
+
try {
|
|
715
|
+
// Node resolves import.meta.url through symlinks but leaves argv[1] as the
|
|
716
|
+
// invoked path. Compare real paths so a linked skill still runs its CLI,
|
|
717
|
+
// normalizing the drive-letter casing that Windows junctions can change.
|
|
718
|
+
return sameMainModulePath(
|
|
719
|
+
realpathSync(process.argv[1]),
|
|
720
|
+
realpathSync(fileURLToPath(import.meta.url))
|
|
721
|
+
);
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (isMainModule()) {
|
|
506
728
|
const args = process.argv.slice(2);
|
|
507
729
|
const fromIdx = args.indexOf('--from');
|
|
508
730
|
const scopeIdx = args.indexOf('--scope');
|
|
509
731
|
const rerollIdx = args.indexOf('--reroll');
|
|
732
|
+
const registerIdx = args.indexOf('--register');
|
|
510
733
|
const modeIdx = args.indexOf('--mode');
|
|
511
734
|
const grainIdx = args.indexOf('--grain');
|
|
512
735
|
const platformIdx = args.indexOf('--platform');
|
|
513
736
|
const candidateCountIdx = args.indexOf('--candidate-count');
|
|
514
737
|
const chosenIdx = args.indexOf('--chosen');
|
|
738
|
+
const kindIdx = args.indexOf('--kind');
|
|
515
739
|
try {
|
|
516
|
-
if (chosenIdx !== -1) {
|
|
740
|
+
if (chosenIdx !== -1 || kindIdx !== -1) {
|
|
517
741
|
// Choice ping: always exits 0, telemetry must never fail a design flow.
|
|
742
|
+
// --kind alone pings a non-challenger outcome (assigned/pick/canon);
|
|
743
|
+
// --chosen alone stays the legacy challenger-win ping.
|
|
518
744
|
const sent = await pingChosen({
|
|
519
|
-
chosenId: args[chosenIdx + 1],
|
|
745
|
+
chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined,
|
|
520
746
|
key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
|
|
521
747
|
scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
|
|
522
748
|
mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined,
|
|
749
|
+
kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined,
|
|
750
|
+
register: registerIdx !== -1 ? args[registerIdx + 1] : undefined,
|
|
523
751
|
});
|
|
524
752
|
process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n');
|
|
753
|
+
// The choice is resolved; this is the last script output the model
|
|
754
|
+
// reads before it decides what to do next, and every run that skipped
|
|
755
|
+
// the comp round did so right here: prose 20 KB into new-work.md lost
|
|
756
|
+
// to "direction locked, building now". So the ping prints the next
|
|
757
|
+
// mandatory step from the recorded build path, and the phase machine
|
|
758
|
+
// takes it from there.
|
|
759
|
+
process.stdout.write(nextStepAfterChoice({
|
|
760
|
+
key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
|
|
761
|
+
scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
|
|
762
|
+
}));
|
|
525
763
|
} else {
|
|
764
|
+
// A dealt roll leaves a marker the build phase clears: context.mjs and
|
|
765
|
+
// detect.mjs read it and refuse to treat page work as done while a
|
|
766
|
+
// direction is chosen but the build never started (COMP_ROUND_OPEN).
|
|
767
|
+
try {
|
|
768
|
+
const { mkdirSync, writeFileSync: wf } = await import('node:fs');
|
|
769
|
+
if (scopeIdx !== -1 && args[scopeIdx + 1] === 'direction') {
|
|
770
|
+
mkdirSync(resolve(process.cwd(), '.impeccable', 'build'), { recursive: true });
|
|
771
|
+
wf(resolve(process.cwd(), '.impeccable', 'build', 'pending.json'), JSON.stringify({ scope: 'direction', at: new Date().toISOString() }, null, 2));
|
|
772
|
+
}
|
|
773
|
+
} catch { /* marker is best-effort */ }
|
|
526
774
|
// Mechanical init gate: prose alone does not keep a model from dealing
|
|
527
775
|
// before init, and fresh repos produced exactly that skip (the model
|
|
528
776
|
// rolled directions with no PRODUCT.md, so nothing grounded the fusion).
|
|
@@ -542,6 +790,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
542
790
|
? args[fromIdx + 1]
|
|
543
791
|
: (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
|
|
544
792
|
reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
|
|
793
|
+
register: registerIdx !== -1 ? args[registerIdx + 1] : null,
|
|
545
794
|
mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
|
|
546
795
|
grain: grainIdx !== -1 ? args[grainIdx + 1] : null,
|
|
547
796
|
platform: platformIdx !== -1 ? args[platformIdx + 1] : null,
|
|
@@ -553,6 +802,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
553
802
|
process.exitCode = 1;
|
|
554
803
|
}
|
|
555
804
|
// A raced-out fetch may still hold a socket; exit explicitly so the CLI
|
|
556
|
-
// never lingers on a dead network path after output is written.
|
|
805
|
+
// never lingers on a dead network path after output is written. Destroy
|
|
806
|
+
// fetch's global undici dispatcher first: process.exit() with a live
|
|
807
|
+
// keep-alive socket trips a libuv assertion on Windows and aborts the
|
|
808
|
+
// process after a successful roll (nodejs/node#56645).
|
|
809
|
+
const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')];
|
|
810
|
+
if (dispatcher && typeof dispatcher.destroy === 'function') {
|
|
811
|
+
try { await dispatcher.destroy(); } catch { /* exit regardless */ }
|
|
812
|
+
}
|
|
557
813
|
process.exit(process.exitCode ?? 0);
|
|
558
814
|
}
|
|
@@ -22,7 +22,7 @@ import path from 'node:path';
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { execFileSync } from 'node:child_process';
|
|
24
24
|
import { loadContext, extractPlatform } from './context.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
|
26
26
|
|
|
27
27
|
/** Is there code here at all, or just context files / an empty repo? */
|
|
28
28
|
function hasCode(cwd) {
|
|
@@ -34,34 +34,25 @@ function hasCode(cwd) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
|
39
|
-
* Parses the small frontmatter for score + P0/P1 counts.
|
|
37
|
+
* Summarize the most recent critique snapshot across all targets.
|
|
40
38
|
*/
|
|
41
39
|
function latestCritique(cwd) {
|
|
42
40
|
try {
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
const
|
|
46
|
-
if (!files.length) return null;
|
|
47
|
-
const newest = files[files.length - 1];
|
|
48
|
-
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
|
49
|
-
const front = text.split('---')[1] || '';
|
|
50
|
-
const get = (k) => {
|
|
51
|
-
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
|
52
|
-
return m ? m[1].trim() : null;
|
|
53
|
-
};
|
|
41
|
+
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
|
42
|
+
if (!latest) return null;
|
|
43
|
+
const get = (key) => latest.meta[key] ?? null;
|
|
54
44
|
const num = (v) => {
|
|
45
|
+
if (v == null || (typeof v === 'string' && v.trim() === '')) return null;
|
|
55
46
|
const n = Number(v);
|
|
56
47
|
return Number.isFinite(n) ? n : null;
|
|
57
48
|
};
|
|
58
49
|
return {
|
|
59
50
|
slug: get('slug'),
|
|
60
|
-
score: num(get('score')),
|
|
61
|
-
p0: num(get('p0')),
|
|
62
|
-
p1: num(get('p1')),
|
|
51
|
+
score: num(get('total_score') ?? get('score')),
|
|
52
|
+
p0: num(get('p0_count') ?? get('p0')),
|
|
53
|
+
p1: num(get('p1_count') ?? get('p1')),
|
|
63
54
|
timestamp: get('timestamp'),
|
|
64
|
-
file: path.relative(cwd, path
|
|
55
|
+
file: path.relative(cwd, latest.path),
|
|
65
56
|
};
|
|
66
57
|
} catch {
|
|
67
58
|
return null;
|