@natjswenson/devlog 0.11.2 → 0.13.0

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/CHANGELOG.md CHANGED
@@ -2,21 +2,89 @@
2
2
 
3
3
  All notable changes to `@natjswenson/devlog` are documented here.
4
4
 
5
- ## 0.11.2 (2026-07-28) — actually render the README on npmjs.com
6
-
7
- - **Fixed: the npm page still showed "No README data found" after 0.11.1.** That
8
- release put README.md, LICENSE and CHANGELOG.md into the tarball (verified by
9
- downloading it), which fixed what `npm install` delivers but not the website.
10
- npm reads README.md to populate the registry manifest's `readme` field when it
11
- builds the publish manifest, which happens BEFORE the package's own `prepack`
12
- hook runs. So the tarball had the file and the registry record did not.
13
- Worse than merely empty: the stored value was the literal string
14
- `ERROR: No README data found!`, and npmjs.com falls back to reading the
15
- tarball only when that field is *absent* (compare `zod`, which renders fine
16
- with no packument readme at all), so the error string kept winning.
17
- The release workflow now stages the three files into the package directory
18
- before `npm publish` is invoked at all, rather than relying on `prepack`.
19
- Code unchanged.
5
+ ## 0.13.0 (2026-07-29) — a publish run now proves the entry is live
6
+
7
+ Publishing reported success on a post that was never reachable. The content, cover
8
+ and manifest all landed correctly, the push succeeded, every command returned `ok`,
9
+ and the entry 404'd for an hour because the site's own project registry had never
10
+ been told the project existed. A push is not a route, and this release stops the
11
+ skill treating the two as the same thing.
12
+
13
+ - **Added: `publish-entry` returns `firstEntryForProject`.** True when the call
14
+ created a project's first live entry, which is exactly when a rendering site needs
15
+ a new registry row. A project holding only tombstones still counts as first, since
16
+ a retired entry never implied a route. Previously nothing distinguished a project's
17
+ first publish from its fiftieth, so the one moment registration is required looked
18
+ identical to every run where it isn't.
19
+ - **Added: `siteUrl` config field** (settable, optional, trailing slash stripped,
20
+ http(s) only) plus `siteUrl` in the scan output. The repo name is not the site name
21
+ — a repo named `example.io` is commonly served at `example.com` — so a publish run
22
+ had no way to reach the page it had just published.
23
+ - **Added: SKILL.md Generate step 5b (register a project the site has never
24
+ rendered).** Find the site's registry, add the project, and *build the clone to
25
+ confirm the route exists* before pushing. Also warns that registry position can
26
+ decide feed order: a site sorting by date alone falls back to load order for
27
+ same-date entries, so a project appended last can render its newest entry last.
28
+ - **Added: SKILL.md Generate step 6 (verify the entry is live).** Poll the published
29
+ URL until it returns 200 before reporting success; on a persistent 404, check the
30
+ registry first, then the host's build log, and report it as **not live**. With no
31
+ `siteUrl` configured, the run must say the entry is pushed but unverified rather
32
+ than call it published.
33
+ - **Changed: the confirmation block leads with the verified live URL**, with the repo
34
+ blob URL demoted to secondary, and prints the failure on the `Live:` line rather
35
+ than a URL that may 404.
36
+
37
+ ## 0.12.0 (2026-07-28) — five composition slots; stop defaulting to before/after
38
+
39
+ Minor rather than patch: this adds three new named composition slots, so covers
40
+ generated after it will legitimately look different from covers generated before.
41
+ Style-guide prose only — no code changed.
42
+
43
+ - **Added: three composition slots**, taking the set from two to five. *Axis /
44
+ timeline* for posts about ordering, timing or a race; *nested / containment*
45
+ for scope, sandboxing, or a thing living inside a thing; *field / distribution*
46
+ for many items, sampling, coverage, or one anomaly among many.
47
+ - **Fixed: compositional monoculture across a run.** Two-node before/after fits
48
+ almost any post *badly*, because nearly anything can be framed as a
49
+ transformation — so it was the slot the model kept reaching for. Fifty covers
50
+ that are each individually fine but all split-scene-with-a-connecting-line read
51
+ as one template with the labels swapped. That is the anti-similarity failure at
52
+ the level of the whole publication rather than the individual cover, which
53
+ nothing was checking for. The guide now requires identifying which slot each
54
+ reference cover from `cover-context` used, and picking a different one if two or
55
+ more already share the intended slot.
56
+ - **Added: no rounded incidental shapes.** The no-rounded-corners rule now names
57
+ the shapes reached for without thinking — speech bubbles, callout balloons, pill
58
+ badges, tooltips, rounded terminal chrome — and says to check every `rx`/`ry`
59
+ and `border-radius` before rendering. One rounded bubble in an otherwise square
60
+ composition collapses the cover into web-app chrome.
61
+ - **Added: never print a number the post did not state.** A figure that looks like
62
+ a measurement ("0 edits left", "3x faster", "~40ms") must appear literally in the
63
+ `## Shipped` text or the summary; fair entailment is not enough, because set in
64
+ mono on a cover it reads as something measured, and a reader who goes looking
65
+ for it will not find it. When the point is zero, draw the absence rather than
66
+ asserting a count.
67
+
68
+ ## 0.11.2 (2026-07-28) — attempted npm-page fix; did NOT work
69
+
70
+ - **Attempted and failed: making npmjs.com render the README.** 0.11.1 put
71
+ README.md, LICENSE and CHANGELOG.md into the tarball (verified by downloading
72
+ the published artifact), which fixed what `npm install` delivers. This release
73
+ additionally staged those files into the package directory before
74
+ `npm publish` was invoked, on the theory that npm populates the registry
75
+ manifest's `readme` field from disk at that point. **It did not work.** After
76
+ publishing, `npm view <pkg> readme` still returns the literal string
77
+ `ERROR: No README data found!` for both packages.
78
+ That field appears to be sticky: it was set once by an early release that had
79
+ no README, and republishing with one present does not overwrite it. Since
80
+ npmjs.com only falls back to reading the tarball when the field is *absent*
81
+ (compare `zod`, which renders fine with none at all), the stale error string
82
+ keeps winning.
83
+ The ineffective staging step was reverted in the release workflow; the
84
+ `prepack` hook stays, because the tarball fix is real and verified. **The npm
85
+ web page for both packages is still blank** — an open issue, likely needing
86
+ npm support to clear the cached field.
87
+ Code unchanged in both skills.
20
88
 
21
89
  ## 0.11.1 (2026-07-28) — publish the README, LICENSE and CHANGELOG to npm
22
90
 
package/SKILL.md CHANGED
@@ -41,12 +41,19 @@ Map the user's request onto the CLI — never hand-edit `config.json`:
41
41
  | Show config | `npx -y @natjswenson/devlog@latest config --json` |
42
42
  | Add a project | `npx -y @natjswenson/devlog@latest add-project --yes --path <abs-path> [--key K] [--remote O/R] [--label L] [--tag-prefix P] [--path-filter F] [--private]` |
43
43
  | Remove a project | `npx -y @natjswenson/devlog@latest remove-project <key> --yes` |
44
- | Change a setting | `npx -y @natjswenson/devlog@latest set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
44
+ | Change a setting | `npx -y @natjswenson/devlog@latest set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `siteUrl`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
45
45
 
46
46
  `targetDir` is the subdirectory of `targetRepo` that holds the devlog content tree
47
47
  (e.g. `content/devlog` when the target is the site repo itself); unset/empty means the
48
48
  repo root. Set it with `set targetDir content/devlog`, clear it with `set targetDir ''`.
49
49
 
50
+ `siteUrl` is the public base URL that repo is *served* at, with no trailing slash — used
51
+ to verify a published entry actually renders (Generate step 6). **The repo name is not the
52
+ site name**: a repo called `example.io` is commonly served at `example.com`, so take this
53
+ from a working page, not from the repo. Without it a run can push but cannot confirm
54
+ anything went live. Set with `set siteUrl 'https://example.com'`, clear with
55
+ `set siteUrl ''`.
56
+
50
57
  For **add-project**: resolve the path first (the repo the user named, or the cwd), then
51
58
  detect what the CLI will use — key = directory basename, remote = `git -C '<path>' remote
52
59
  get-url origin`. In a monorepo, suggest a `--path-filter` (the project's subdir) and a
@@ -412,7 +419,65 @@ rm -rf '<abs-tmp>'
412
419
 
413
420
  If the push fails, report the error and stop — do not retry automatically.
414
421
 
415
- ### Step 6: Confirm
422
+ ### Step 5b: Register a project the site has never rendered before
423
+
424
+ `publish-entry` returns **`firstEntryForProject: true`** when the call created that
425
+ project's first live entry. **A push is not a route.** A site that renders the devlog
426
+ almost always keeps its own registry deciding *which* content directories become pages;
427
+ content on disk that the registry doesn't list is built right past, and the entry 404s
428
+ while every command in this skill reports success. Publishing a project's first entry is
429
+ therefore a two-part job, and the CLI can only do the first part.
430
+
431
+ When `firstEntryForProject` is true, **before the push**:
432
+
433
+ 1. Find the registry in the clone. It is a source file, not content — grep the site's
434
+ `src/` for the existing project keys (`grep -rn '<a-known-project-key>' <clone>/src`).
435
+ In an Astro/Next-style site it is typically a `PROJECTS` array the entry loader maps
436
+ over.
437
+ 2. Add the new project, matching the shape of the neighbouring rows exactly.
438
+ 3. **Build the site in the clone and confirm the route exists** (`npm ci && npx astro
439
+ build`, then check the output directory contains the new `<project>/<version>` path).
440
+ A registry edit that doesn't produce a route is not done.
441
+ 4. Run the site's own test suite if it has one, and commit the registry change together
442
+ with the content in the same push.
443
+
444
+ If you cannot find a registry, say so plainly rather than assuming there isn't one; the
445
+ verification in Step 6 is what actually settles it.
446
+
447
+ **Registry position can decide feed order, not just visibility.** If the site sorts
448
+ entries by date alone, same-date entries fall back to their load order, which is registry
449
+ order — so a project appended to the end can render its newest entry last. After the build
450
+ in step 3, check that the new entry appears where its date and publish numeral say it
451
+ should, and report it if not.
452
+
453
+ ### Step 6: Verify the entry is actually live
454
+
455
+ **Do not report a publish as successful until the published URL returns 200.** The push
456
+ succeeding, the manifest updating, and the site serving the post are three different
457
+ things, and this skill has previously reported success on all of the first two while the
458
+ post 404'd.
459
+
460
+ When `siteUrl` is set in config (it is echoed in the scan output):
461
+
462
+ ```bash
463
+ curl -s -o /dev/null -w '%{http_code}' -L '<siteUrl>/devlog/<project>/<version>/'
464
+ ```
465
+
466
+ Poll every ~45s for up to ~5 minutes; deploys are not instant. Then:
467
+
468
+ - **200** → done, report the live URL as the primary link.
469
+ - **Still 404 after the window** → the deploy failed or the entry is not routed. Check
470
+ `firstEntryForProject`/the registry from Step 5b first, since that is the most common
471
+ cause, then the host's build log. Report it as **not live**, with what you checked.
472
+ - **`siteUrl` unset** → say the entry is pushed but unverified, give the repo URL, and
473
+ offer to set it: `npx -y @natjswenson/devlog@latest set siteUrl 'https://example.com'`.
474
+ Never describe an unverified push as published.
475
+
476
+ The `/devlog/<project>/<version>/` path is this skill's default convention. If the site
477
+ routes differently, take the pattern from a URL of an already-published entry rather than
478
+ assuming this one.
479
+
480
+ ### Step 7: Confirm
416
481
 
417
482
  ```
418
483
  Release dev log entries published
@@ -420,13 +485,13 @@ Release dev log entries published
420
485
  Project: <key>
421
486
  Releases: <version>, ...
422
487
  Judged weaknesses: <residuals from Step 4, or "none">
423
- URL: https://github.com/<targetRepo>/blob/<branch>/<targetDir-prefix><key>/<version>.md
488
+ Live: <siteUrl>/devlog/<key>/<version>/ (verified 200)
489
+ Source: https://github.com/<targetRepo>/blob/<branch>/<targetDir-prefix><key>/<version>.md
424
490
  ```
425
491
 
426
- (`<targetDir-prefix>` is `<targetDir>/` when set, empty otherwise.) When the target is
427
- a site repo that auto-deploys on push (e.g. Cloudflare Pages watching `main`), the
428
- publish push itself triggers the rebuild mention that the entry goes live with the
429
- next deploy.
492
+ (`<targetDir-prefix>` is `<targetDir>/` when set, empty otherwise.) Report the **verified
493
+ live URL** as the primary link; the repo blob URL is the secondary one. If Step 6 could
494
+ not verify, say so on the `Live:` line instead of printing a URL that may 404.
430
495
 
431
496
  ## Security rules
432
497
 
@@ -82,15 +82,39 @@ Never leave the band empty under a one-line headline. Whitespace is part of the
82
82
  only when it's *between* composed elements — a gap with nothing on either side of it is
83
83
  just a hole.
84
84
 
85
- **Two named composition slots** — pick one per post:
85
+ **Five named composition slots** — pick one per post:
86
86
  - **Single centered hero** — one freehand mechanism, nothing else, inside the hero zone.
87
87
  - **Two-node before/after** — a left node, a right node, and a connecting line, all
88
88
  three freehand shapes (never catalog icons) — for a post about a transformation or a
89
89
  fix.
90
+ - **Axis / timeline** — one horizontal (or vertical) run with marks, labels, and events
91
+ positioned along it. For anything about *ordering*, *timing*, or *a race*: what fired
92
+ when, what arrived late, what overlapped. The marks carry the meaning, not boxes.
93
+ - **Nested / containment** — one shape inside another, or a stack of layers, showing
94
+ what wraps what, what's isolated from what, or where a boundary sits. For posts about
95
+ scope, sandboxing, encapsulation, or a thing living inside a thing.
96
+ - **Field / distribution** — many small marks whose *arrangement* is the point: a
97
+ scatter, a cluster with an outlier, a grid with holes, a shape emerging from noise.
98
+ For posts about many items, sampling, coverage, or one anomaly among many.
90
99
 
91
100
  These are placement/proportion guidance, not literal templates — the actual shapes
92
101
  inside each slot are still freehand per post.
93
102
 
103
+ **Do not default to two-node.** It is the easiest slot to reach for and it fits almost
104
+ any post *badly*, because nearly everything can be framed as before/after. That framing
105
+ is the single biggest threat to this set: fifty covers that are each individually fine
106
+ but all split-scene-with-a-connecting-line read as one template with the labels swapped,
107
+ which is the anti-similarity failure at the level of the whole publication rather than
108
+ the individual cover.
109
+
110
+ So, before you commit to a slot: **look at the reference covers `cover-context` handed
111
+ you and identify which slot each one used.** If two or more of them used the slot you
112
+ were about to pick, pick a different one — and only override that if the post's mechanism
113
+ genuinely cannot be drawn any other way, which is rarer than it feels. Reach for
114
+ single-centered-hero more often than instinct suggests; one well-drawn mechanism with
115
+ nothing beside it is usually stronger than a comparison, and it is the slot most likely
116
+ to be under-used across a long run.
117
+
94
118
  **A third option, when the post is genuinely about code or a terminal session:** the
95
119
  `.term` panel treatment (see Palette below) may fill some or all of `#hero-zone` instead
96
120
  of a freehand mechanism — a real, believable command/output snippet drawn from the
@@ -258,13 +282,24 @@ summary as on-image text.
258
282
  edges, fold lines, or any texture overlay — the paper is a flat hex, not a vintage
259
283
  poster prop.
260
284
  - Don't round any corner or drop any shadow, on the illustration or anywhere else on the
261
- cover — PRESS structure comes from rules and whitespace, never rounded chrome.
285
+ cover — PRESS structure comes from rules and whitespace, never rounded chrome. This
286
+ includes *incidental* shapes you reach for without thinking: speech bubbles, callout
287
+ balloons, pill badges, tooltips, rounded terminal chrome. A speech bubble is a
288
+ rectangle with a triangular tail, not a lozenge. Check every `rx`/`ry` on an SVG rect
289
+ and every `border-radius` before you render — a single rounded bubble in an otherwise
290
+ square composition is the tell that collapses the whole cover into web-app chrome.
262
291
  - Don't let orange spread across the cover — one signature moment (see The accent law),
263
292
  never orange as a fill color for multiple shapes, never a background wash.
264
293
  - Don't set the headline, eyebrow, or numerals in the serif voice — that voice is for
265
294
  commentary only; structure is always the display face.
266
295
  - Don't invent editorial props that aren't backed by real data: no fabricated issue
267
296
  numbers, no barcodes, no pull-quotes that aren't an actual quote from the post.
297
+ - Don't print a number the post didn't state. A figure that *looks* like a measurement
298
+ ("0 edits left", "3x faster", "~40ms") must appear literally in the `## Shipped` text
299
+ or the summary. A fair entailment is not enough: once it's set in mono on the cover it
300
+ reads as something that was measured, and a reader who goes looking for it in the post
301
+ won't find it. If the point is "none" or "zero," draw the absence — an empty frame, a
302
+ severed line, a gap where marks used to be — rather than asserting a count.
268
303
  - Don't leave old terminal furniture lying around outside a `.term` panel — no blinking
269
304
  cursor, no bare `_` suffix, no stray `$` prompt as decoration. The dark palette now
270
305
  belongs to exactly one place, the `.term` panel, and only when it's real code.
@@ -34,6 +34,7 @@ const SETTERS = {
34
34
  targetDir: (c, v) => (v === '' ? omit(c, 'targetDir') : { ...c, targetDir: v }),
35
35
  gitAuthor: (c, v) => ({ ...c, gitAuthor: v }),
36
36
  githubUser: (c, v) => ({ ...c, githubUser: v }),
37
+ siteUrl: (c, v) => (v === '' ? omit(c, 'siteUrl') : { ...c, siteUrl: v.replace(/\/+$/, '') }),
37
38
  voicePath: (c, v) => (v === '' ? omit(c, 'voicePath') : { ...c, voicePath: expandHome(v) }),
38
39
  'deepDive.minSources': (c, v) => {
39
40
  const n = Number(v);
package/lib/core.mjs CHANGED
@@ -125,6 +125,19 @@ export function validateConfig(config) {
125
125
  throw new Error(`targetDir must be a relative path like "content/devlog" (no leading/trailing slash, no '..'): got ${JSON.stringify(config.targetDir)}`);
126
126
  }
127
127
  }
128
+ if ('siteUrl' in config) {
129
+ // Optional: the public base URL the target repo is served at, used only to
130
+ // confirm a published entry actually renders. The repo name is not the site
131
+ // name (a repo called `example.io` can be served at `example.com`), and
132
+ // pushing content is not the same as the site serving it, so this is the
133
+ // only way a publish run can verify itself rather than assume.
134
+ // Interpolated into a shell curl, hence the tight charset.
135
+ if (typeof config.siteUrl !== 'string'
136
+ || !/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._~/-]*)?$/.test(config.siteUrl)
137
+ || config.siteUrl.endsWith('/')) {
138
+ throw new Error(`siteUrl must be an http(s) URL with no trailing slash, e.g. "https://example.com": got ${JSON.stringify(config.siteUrl)}`);
139
+ }
140
+ }
128
141
  if ('voicePath' in config) {
129
142
  // Optional: directory holding the voice profile used to write entries. Read by
130
143
  // the skill with the Read tool only — never shell-interpolated — so the only
@@ -221,6 +221,14 @@ export function publishEntry({ cloneDir, project, version, entryPath, coverImage
221
221
  }
222
222
 
223
223
  const file = `${version}.md`;
224
+ // Whether this project had any live entry before this call. A site that
225
+ // renders the devlog typically keeps its own registry of which projects to
226
+ // render (content on disk is not the same as a route), and that registry is
227
+ // the thing a first-ever publish silently misses: the entry lands, the build
228
+ // walks past it, and the post 404s while every check reports success.
229
+ // Reported back so the caller can require registration exactly once per
230
+ // project, instead of relying on whoever is publishing to remember.
231
+ const hadLiveEntries = manifest.entries.some((e) => e && !e.removed);
224
232
  // Idempotent: legacy manifests may already reference this file/version even
225
233
  // when the .md was missing — never duplicate an index row.
226
234
  const already = manifest.entries.some((e) => e && (e.file === file || e.version === version));
@@ -247,7 +255,13 @@ export function publishEntry({ cloneDir, project, version, entryPath, coverImage
247
255
  manifestUpdated = true;
248
256
  }
249
257
 
250
- return { written: destPath, manifestUpdated, coverWritten: !!coverFile, no };
258
+ return {
259
+ written: destPath,
260
+ manifestUpdated,
261
+ coverWritten: !!coverFile,
262
+ no,
263
+ firstEntryForProject: manifestUpdated && !hadLiveEntries,
264
+ };
251
265
  }
252
266
 
253
267
  // Editorially retire a release: after an entry is manually moved, consolidated,
package/lib/scan.mjs CHANGED
@@ -288,6 +288,11 @@ export function scanAll(config, { projectKey = null, fetch = true, getExisting =
288
288
  // skill appends it to the publish clone path (`--clone <clone>/<targetDir>`).
289
289
  targetDir: config.targetDir || '',
290
290
  branch,
291
+ // Public base URL the target repo is served at, when configured — the skill
292
+ // uses it to confirm a published entry actually renders. null means the run
293
+ // can push but cannot verify, which the skill has to say out loud rather
294
+ // than report an unverified publish as a success.
295
+ siteUrl: config.siteUrl || null,
291
296
  deepDive: resolveDeepDive(config),
292
297
  voicePath: config.voicePath || null,
293
298
  projects: results,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natjswenson/devlog",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "description": "Release dev log generator \u2014 Claude Code skill + preview app for publishing version-release dev logs, written in your voice, to your site",
5
5
  "license": "MIT",
6
6
  "author": "Nate Swenson",