@natjswenson/press 0.7.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to the **press** skill are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
6
  this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.1] - 2026-08-01
9
+
10
+ ### Fixed
11
+
12
+ - **Declaring a new target broke every consumer's CI.** 0.7.0 added README
13
+ targets for the external repos without seeding the regions, so the moment a
14
+ consumer's pin reached 0.7.0 its `check` failed `missing`. `propagate` now
15
+ **seeds** a region the consumer does not have yet, in the same pull request
16
+ that raises the pin — the two are one change, not two. Only targets declaring
17
+ an `init` anchor are seedable, so a deliberately deleted region is still
18
+ reported rather than silently recreated.
19
+ - **`check` now enforces the recorded version, not just the values.** In-repo
20
+ regions sat at `v0.1.0` through six releases with a green check, because only
21
+ content was compared. One version per consumer means `check` has to say so.
22
+ - New `init.insertAfter` anchor, so a seeded README badge lands under the title
23
+ instead of at the bottom of the file.
24
+
25
+ ### Added
26
+
27
+ - Version badges for the in-repo skills' READMEs (`city-report`, `resume`,
28
+ `ghostwriter`, `ghostwriter-x`, `devlog`), and `press` itself is now listed in
29
+ the monorepo's root README skill table.
30
+ - `natejswenson` has no README target on purpose: its README is generated by
31
+ `build_svg.py` and a region there would be clobbered on the next build.
32
+
8
33
  ## [0.7.0] - 2026-08-01
9
34
 
10
35
  ### Added
@@ -294,6 +319,7 @@ source of truth and a CI drift gate.
294
319
  rasterised cards, whose eyebrow legitimately runs at `.16em`. Scoped rather
295
320
  than waived, and caught by linting the real shipped corpus.
296
321
 
322
+ [0.7.1]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.7.1
297
323
  [0.7.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.7.0
298
324
  [0.6.1]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.6.1
299
325
  [0.6.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.6.0
package/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: press
3
3
  description: The one brand system for everything produced in Claude — design tokens, the visual laws, the run-presentation contract, and the universal voice core. Use when composing or restyling any artifact (report, résumé, card, cover, PDF, HTML page, chart), when asked about brand colors, fonts, the accent law or "the PRESS look", when adding a new skill that renders anything, or when a brand value needs to change everywhere at once. Also handles "check the brand is in sync", "why do my colors differ", and onboarding a new consumer repo.
4
4
  user_invocable: true
5
- version: 0.7.0
5
+ version: 0.7.1
6
6
  ---
7
7
 
8
8
  # /press — the brand system
package/lib/check.mjs CHANGED
@@ -40,10 +40,18 @@ export function checkTarget(target, root, tokens, version) {
40
40
  const expected = emitBody(tokens, target.emitter, target.params ?? {}, { version })
41
41
  .replace(/\s+$/, '');
42
42
  const actual = found.body.replace(/\s+$/, '');
43
- if (expected === actual) {
44
- return { target, path, status: 'ok', writtenBy: found.version };
43
+ if (expected !== actual) {
44
+ return { target, path, status: 'drift', diff: lineDiff(expected, actual), writtenBy: found.version };
45
45
  }
46
- return { target, path, status: 'drift', diff: lineDiff(expected, actual), writtenBy: found.version };
46
+ // The values match, but the region must also record the release it belongs
47
+ // to. Without this the version is decoration: in-repo regions sat at v0.1.0
48
+ // through six releases with a green check, which is precisely the "three
49
+ // different version numbers and no way to read them" problem this policy
50
+ // exists to end. One version per consumer means check has to say so.
51
+ if (found.version !== version) {
52
+ return { target, path, status: 'stale-version', writtenBy: found.version };
53
+ }
54
+ return { target, path, status: 'ok', writtenBy: found.version };
47
55
  }
48
56
 
49
57
  export function checkAll({ tokens, targets, root, ids, version }) {
@@ -86,6 +94,7 @@ export function lineDiff(expected, actual) {
86
94
  export const EXPLAIN = {
87
95
  ok: 'in sync',
88
96
  drift: 'region content differs from what the tokens produce',
97
+ 'stale-version': 'values are correct but the region records an older press release — re-emit',
89
98
  missing: 'file has no press region — the generated block was removed',
90
99
  absent: 'declared file does not exist at this path',
91
100
  corrupt: 'region markers are malformed',
package/lib/propagate.mjs CHANGED
@@ -25,7 +25,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
 
27
27
  import { emitBody } from './emit.mjs';
28
- import { findRegion, spliceRegion } from './region.mjs';
28
+ import { findRegion, initRegion, spliceRegion } from './region.mjs';
29
29
  import { selectTargets, targetPath } from './targets.mjs';
30
30
 
31
31
  /** The npm reference consumers pin in CI, e.g. `@natjswenson/press@0.3.0`. */
@@ -47,7 +47,32 @@ export function propagate({ tokens, targets, root, version, dryRun = false }) {
47
47
  const before = readFileSync(path, 'utf8');
48
48
  const found = findRegion(before, target.region, target.syntax);
49
49
  if (!found) {
50
- regions.push({ id: target.id, path: target.path, status: 'missing' });
50
+ // A newly declared target has no region in the consumer yet, and that
51
+ // consumer's `check` starts failing the moment its pin reaches the
52
+ // release that declares it. So the PR that raises the pin must also
53
+ // create the region — the two are one change, not two.
54
+ //
55
+ // This is not "silently creating": it lands in a reviewed pull request
56
+ // alongside the pin bump. A region that was deliberately DELETED is a
57
+ // different case, and one this cannot distinguish, which is why the
58
+ // target must declare an `init` anchor to be seedable at all.
59
+ if (!target.init) {
60
+ regions.push({ id: target.id, path: target.path, status: 'missing' });
61
+ continue;
62
+ }
63
+ const body = emitBody(tokens, target.emitter, target.params ?? {}, { version });
64
+ if (!dryRun) {
65
+ writeFileSync(path, initRegion(before, target.region, target.syntax, body, version, target.init), 'utf8');
66
+ }
67
+ regions.push({
68
+ id: target.id,
69
+ path: target.path,
70
+ changed: true,
71
+ brandChanged: false,
72
+ versionChanged: true,
73
+ status: dryRun ? 'would seed' : 'seeded',
74
+ wroteBy: null,
75
+ });
51
76
  continue;
52
77
  }
53
78
  const body = emitBody(tokens, target.emitter, target.params ?? {}, { version });
package/lib/region.mjs CHANGED
@@ -153,7 +153,22 @@ export function initRegion(text, region, syntaxName, body, version, anchor = {})
153
153
  throw new RegionError(`file already has a press:${region} region — use emit without --init`);
154
154
  }
155
155
  const block = renderRegion(region, syntaxName, body, version);
156
- const { replaceFrom, replaceTo } = anchor;
156
+ const { replaceFrom, replaceTo, insertAfter } = anchor;
157
+
158
+ // Seeding a brand-new region (a README badge) rather than taking over a
159
+ // hand-written block: place it after the first line matching `insertAfter`,
160
+ // usually the title, instead of appending to the bottom where nobody looks.
161
+ if (insertAfter) {
162
+ const lines = text.split('\n');
163
+ const re = new RegExp(insertAfter);
164
+ const at = lines.findIndex((l) => re.test(l));
165
+ if (at === -1) throw new RegionError(`anchor insertAfter /${insertAfter}/ matched no line`);
166
+ let i = at + 1;
167
+ while (i < lines.length && lines[i].trim() === '') i += 1;
168
+ lines.splice(i, 0, ...block.split('\n'), '');
169
+ return lines.join('\n');
170
+ }
171
+
157
172
  if (!replaceFrom) return `${text.replace(/\s+$/, '')}\n\n${block}\n`;
158
173
 
159
174
  const lines = text.split('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natjswenson/press",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "One brand system — tokens, laws, run presentation and voice core — generated into every consumer with a CI drift gate",
5
5
  "license": "MIT",
6
6
  "author": "Nate Swenson",
package/targets.json CHANGED
@@ -395,8 +395,7 @@
395
395
  ]
396
396
  },
397
397
  "init": {
398
- "replaceFrom": "^# --- PRESS tokens",
399
- "replaceTo": "^\\}$"
398
+ "insertAfter": "^# "
400
399
  }
401
400
  },
402
401
  {
@@ -408,6 +407,9 @@
408
407
  "emitter": "version-badge",
409
408
  "params": {
410
409
  "what": "The site's colour, type and panel tokens"
410
+ },
411
+ "init": {
412
+ "insertAfter": "^# "
411
413
  }
412
414
  },
413
415
  {
@@ -420,7 +422,10 @@
420
422
  "params": {
421
423
  "what": "The report's colour and type tokens"
422
424
  },
423
- "github": "local-budget"
425
+ "github": "local-budget",
426
+ "init": {
427
+ "insertAfter": "^# "
428
+ }
424
429
  },
425
430
  {
426
431
  "id": "local-fitness-readme",
@@ -431,18 +436,81 @@
431
436
  "emitter": "version-badge",
432
437
  "params": {
433
438
  "what": "The brief and report-card colour and type tokens"
439
+ },
440
+ "init": {
441
+ "insertAfter": "^# "
434
442
  }
435
443
  },
436
444
  {
437
- "id": "natejswenson-readme",
438
- "repo": "natejswenson",
439
- "path": "README.md",
445
+ "id": "city-report-readme",
446
+ "repo": "claude-skills",
447
+ "path": "skills/city-report/README.md",
448
+ "region": "version",
449
+ "syntax": "md",
450
+ "emitter": "version-badge",
451
+ "params": {
452
+ "what": "The report's colour and type tokens"
453
+ },
454
+ "init": {
455
+ "insertAfter": "^# "
456
+ }
457
+ },
458
+ {
459
+ "id": "resume-readme",
460
+ "repo": "claude-skills",
461
+ "path": "skills/resume/README.md",
462
+ "region": "version",
463
+ "syntax": "md",
464
+ "emitter": "version-badge",
465
+ "params": {
466
+ "what": "The résumé themes' colour and type tokens"
467
+ },
468
+ "init": {
469
+ "insertAfter": "^# "
470
+ }
471
+ },
472
+ {
473
+ "id": "ghostwriter-readme",
474
+ "repo": "claude-skills",
475
+ "path": "skills/ghostwriter/README.md",
440
476
  "region": "version",
441
477
  "syntax": "md",
442
478
  "emitter": "version-badge",
443
479
  "params": {
444
- "what": "The profile tiles' colour tokens and vendored faces"
480
+ "what": "The card system's colour, type and terminal tokens"
481
+ },
482
+ "init": {
483
+ "insertAfter": "^# "
484
+ }
485
+ },
486
+ {
487
+ "id": "ghostwriter-x-readme",
488
+ "repo": "claude-skills",
489
+ "path": "skills/ghostwriter-x/README.md",
490
+ "region": "version",
491
+ "syntax": "md",
492
+ "emitter": "version-badge",
493
+ "params": {
494
+ "what": "The card system's colour, type and terminal tokens"
495
+ },
496
+ "init": {
497
+ "insertAfter": "^# "
498
+ }
499
+ },
500
+ {
501
+ "id": "devlog-readme",
502
+ "repo": "claude-skills",
503
+ "path": "skills/devlog/README.md",
504
+ "region": "version",
505
+ "syntax": "md",
506
+ "emitter": "version-badge",
507
+ "params": {
508
+ "what": "The cover-image palette"
509
+ },
510
+ "init": {
511
+ "insertAfter": "^# "
445
512
  }
446
513
  }
447
- ]
514
+ ],
515
+ "$readme_note": "natejswenson has no README target on purpose: its README.md is generated by build_svg.py, so a region there would be clobbered on the next build. Its press version is visible in build_svg.py's own region marker instead."
448
516
  }