@plot-pm/board 0.11.0 → 0.12.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.
@@ -9,10 +9,11 @@
9
9
  # THE ONLY AUTOMATIC WRITE THIS SYSTEM GRANTS, and it is granted for three
10
10
  # verified reasons rather than for convenience:
11
11
  #
12
- # 1. `-merge` KEEPS THE FILE VALID. `.gitattributes` marks the artifact
12
+ # 1. `-merge` KEEPS THE FILE VALID. `.gitattributes` marks every bundle
13
13
  # `-merge`, so git keeps one side whole and writes NO conflict markers.
14
14
  # The artifact stays buildable JavaScript *through* a conflict — which is
15
- # why a script may touch it at all.
15
+ # why a script may touch it at all. `scripts/check-bundle-attributes.sh`
16
+ # is the gate that keeps that true of every bundle rather than of one.
16
17
  # 2. THE REBUILD IS DETERMINISTIC. Measured: `build.mjs` embeds no timestamp
17
18
  # and no randomness, so the output does not depend on which side was kept.
18
19
  # 3. CI PROVES IT. The no-diff gate fails the build if the committed artifact
@@ -52,7 +53,7 @@
52
53
  # AN EMPTY CONFLICT SET IS NOT A REFUSAL ABOUT FILES. Three cases, named apart,
53
54
  # because two of them were once one:
54
55
  #
55
- # exactly the artifact → the licensed case → repair
56
+ # bundles only → the licensed case → repair
56
57
  # other files present → needs judgement → not-artifact-only
57
58
  # empty, no merge ran → nothing was observed → not-observed
58
59
  #
@@ -70,10 +71,45 @@ set -uo pipefail
70
71
 
71
72
  script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
72
73
 
73
- # The one file this script may resolve. Named here as well as in the board's
74
- # contract because the two run in different languages and neither can import
75
- # the other's constant; the pairing is asserted by a test rather than trusted.
76
- ARTIFACT_PATH="skills/plot/scripts/board/board-server.mjs"
74
+ # THE FILES THIS SCRIPT MAY RESOLVE a SET, and derived rather than listed.
75
+ #
76
+ # It was one hardcoded filename until 2026-09-06, and that cost a repair the
77
+ # same day: PR #727 conflicted in `plot-registryd.mjs` — a `-merge` bundle with
78
+ # a deterministic rebuild, exactly the licensed case — and this script refused
79
+ # `not-artifact-only` against a list naming only `board-server.mjs`. The refusal
80
+ # was correct behaviour against a stale list. Hours later the same branch
81
+ # conflicted in `board-server.mjs` and was repaired automatically: same class of
82
+ # conflict, opposite outcome, one filename apart.
83
+ #
84
+ # DERIVED FROM `build.mjs`'S OWN DECLARATIONS, by the same pipeline
85
+ # `scripts/check-bundle-attributes.sh` uses, because a hand-written list here
86
+ # would be a fourth place to drift — and drift is the defect this replaces. The
87
+ # build declares each output as `const shippedX = path.join(here, '…')`, which
88
+ # is what an author writes when adding a bundle; nothing else has to be
89
+ # remembered. Nine bundles today; `plot-landed.mjs` arrived while the plan that
90
+ # asked for this was still in draft, and the derivation found it.
91
+ #
92
+ # `plot-monitor.mjs` IS DELIBERATELY ABSENT. It is committed and documented, and
93
+ # no `outfile` names it — nothing rebuilds it. Property 2 above is the whole
94
+ # licence, so a file with no deterministic rebuild cannot be on this list. The
95
+ # derivation reads the build, so it cannot ask for it.
96
+ #
97
+ # Still named in the board's contract as well, because the two run in different
98
+ # languages and neither can import the other's constant. The pairing is asserted
99
+ # by a test rather than trusted — and that test now asserts SET EQUALITY, since
100
+ # a set that agrees on one member and differs on another is exactly the drift
101
+ # this replaces.
102
+ #
103
+ # READ FROM THE REPOSITORY BEING REPAIRED, not from this script's own checkout.
104
+ # The script is vendored into the published package, where `packages/` does not
105
+ # exist — and it rebuilds with `pnpm build:board` inside the target repo, so the
106
+ # build that defines the set is the one that will run. Resolved below, once
107
+ # `repo_root` is known.
108
+ bundle_set() { # $1=repo root → one path per line, sorted
109
+ grep -aoE "shipped[A-Za-z]* = path\.join\([^)]*'[^']*'\)" "$1/packages/board/build.mjs" 2>/dev/null \
110
+ | sed -E "s|.*'\.\./\.\./([^']*)'.*|\1|" \
111
+ | sort -u
112
+ }
77
113
 
78
114
  dry_run=0
79
115
  branch=""
@@ -109,6 +145,19 @@ MAIN=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^o
109
145
 
110
146
  repo_root=$(git rev-parse --show-toplevel)
111
147
 
148
+ # THE SET, resolved against the repository this run will rebuild.
149
+ ARTIFACT_PATHS=$(bundle_set "$repo_root")
150
+
151
+ # AN EMPTY DERIVATION REFUSES, and it must: the guard below asks whether every
152
+ # unmerged path is in this set, and against an empty set that question has no
153
+ # true answer to give — but a guard written the other way round would have said
154
+ # yes to everything. The build changing shape, or a checkout with no
155
+ # `packages/`, is a reason to stop rather than a reason to repair blind.
156
+ if [ -z "$ARTIFACT_PATHS" ]; then
157
+ echo "step: no bundles derived from packages/board/build.mjs — refusing"
158
+ finish refused no-bundle-set
159
+ fi
160
+
112
161
  # WHICH WORKTREE HOLDS THIS BRANCH — ASK GIT, do not reconstruct the path from
113
162
  # the branch name.
114
163
  #
@@ -151,7 +200,10 @@ fi
151
200
 
152
201
  if [ "$dry_run" = 1 ]; then
153
202
  echo "step: would use worktree $wt"
154
- echo "step: would merge origin/$MAIN, take a side of $ARTIFACT_PATH, rebuild, test"
203
+ echo "step: would merge origin/$MAIN, take a side of each conflicted bundle, rebuild, test"
204
+ printf 'step: bundle set (%s): %s\n' \
205
+ "$(printf '%s\n' "$ARTIFACT_PATHS" | grep -c .)" \
206
+ "$(printf '%s' "$ARTIFACT_PATHS" | tr '\n' ' ')"
155
207
  echo "step: would push only if pnpm run test:board passes"
156
208
  finish refused dry-run
157
209
  fi
@@ -274,23 +326,53 @@ if [ "$n_unmerged" = "0" ]; then
274
326
  finish refused not-observed
275
327
  fi
276
328
 
277
- # EXACTLY the artifact: one path, that path, nothing else. Not "the artifact
278
- # among the conflicts" — an implementation asking that passes every
279
- # artifact-only case and silently repairs merges that need judgement as a whole.
280
- if [ "$n_unmerged" != "1" ] || [ "$unmerged" != "$ARTIFACT_PATH" ]; then
329
+ # EVERY unmerged path is a bundle: the conflict set is a SUBSET of the bundle
330
+ # set, and nothing else is in it. NOT "a bundle is among the conflicts" — an
331
+ # implementation asking that passes every bundle-only case and silently repairs
332
+ # merges that need judgement as a whole. The claim stayed exact when the list
333
+ # grew from one file to nine; only the thing each path is checked against
334
+ # changed. A merge conflicting in a bundle AND anything else still needs a
335
+ # person, even though one of its files does not.
336
+ #
337
+ # WALKED PER PATH rather than compared as a whole, because the conflict set is
338
+ # an arbitrary subset of nine and there is no single string to compare it to.
339
+ # The direction is what keeps it exact: every element of the observed set must
340
+ # appear in the licensed set, so an unlicensed path can only ever refuse.
341
+ outside=""
342
+ while IFS= read -r conflict; do
343
+ [ -n "$conflict" ] || continue
344
+ if ! printf '%s\n' "$ARTIFACT_PATHS" | grep -qxF -- "$conflict"; then
345
+ outside="${outside}${outside:+ }$conflict"
346
+ fi
347
+ done <<EOF
348
+ $unmerged
349
+ EOF
350
+
351
+ if [ -n "$outside" ]; then
281
352
  git -C "$wt" merge --abort >/dev/null 2>&1 || true
282
- echo "step: conflict set is not exactly the artifact — refusing"
353
+ echo "step: conflict set is not bundles only — refusing"
283
354
  printf 'step: unmerged: %s\n' "$(printf '%s' "$unmerged" | tr '\n' ' ')"
355
+ printf 'step: outside the bundle set: %s\n' "$outside"
284
356
  finish refused not-artifact-only
285
357
  fi
286
358
 
287
- # 3. Take a side. WHICH SIDE CANNOT MATTER — the rebuild overwrites it — and the
288
- # diff is never read. `--theirs` because the command needs a word.
289
- git -C "$wt" checkout --theirs -- "$ARTIFACT_PATH" 2>/dev/null \
290
- || git -C "$wt" checkout --ours -- "$ARTIFACT_PATH" 2>/dev/null \
291
- || true
292
- git -C "$wt" add -- "$ARTIFACT_PATH" 2>/dev/null || true
293
- echo "step: took a side of $ARTIFACT_PATH (either — the rebuild decides)"
359
+ # 3. Take a side of EACH conflicted bundle. WHICH SIDE CANNOT MATTER — the
360
+ # rebuild overwrites it — and the diff is never read. `--theirs` because the
361
+ # command needs a word.
362
+ #
363
+ # Only the paths that actually conflicted, never the whole set: a bundle git
364
+ # merged cleanly has no side to take, and `checkout --theirs` on an unmerged-
365
+ # stage-free path errors rather than doing nothing useful.
366
+ while IFS= read -r conflict; do
367
+ [ -n "$conflict" ] || continue
368
+ git -C "$wt" checkout --theirs -- "$conflict" 2>/dev/null \
369
+ || git -C "$wt" checkout --ours -- "$conflict" 2>/dev/null \
370
+ || true
371
+ git -C "$wt" add -- "$conflict" 2>/dev/null || true
372
+ echo "step: took a side of $conflict (either — the rebuild decides)"
373
+ done <<EOF
374
+ $unmerged
375
+ EOF
294
376
 
295
377
  # 4. Rebuild, in the branch's OWN worktree. This is what makes the kept side
296
378
  # irrelevant, and it is the property CI's no-diff gate then re-checks.
@@ -299,8 +381,19 @@ if ! (cd "$wt" && pnpm build:board >/dev/null 2>&1); then
299
381
  echo "step: rebuild failed — pushing nothing"
300
382
  finish abandoned build-failed
301
383
  fi
302
- git -C "$wt" add -- "$ARTIFACT_PATH" 2>/dev/null || true
303
- echo "step: rebuilt $ARTIFACT_PATH"
384
+ # EVERY bundle is staged after the rebuild, not just the ones that conflicted.
385
+ # `pnpm build:board` regenerates all nine, and a rebuild triggered by one
386
+ # conflict can legitimately move another — the merge brought in source changes
387
+ # for the whole package. Staging only the conflicted paths would leave those
388
+ # modifications unstaged, and CI's no-diff gate would then fail the push for a
389
+ # file this run had already rebuilt correctly.
390
+ while IFS= read -r bundle; do
391
+ [ -n "$bundle" ] || continue
392
+ git -C "$wt" add -- "$bundle" 2>/dev/null || true
393
+ done <<EOF
394
+ $ARTIFACT_PATHS
395
+ EOF
396
+ printf 'step: rebuilt and staged %s bundle(s)\n' "$(printf '%s\n' "$ARTIFACT_PATHS" | grep -c .)"
304
397
 
305
398
  # The merge commit exists only once the rebuild has produced the artifact it
306
399
  # will carry. Committing before the build would leave a commit holding a stale