@bookedsolid/rea 0.6.1 → 0.7.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/.husky/pre-push +59 -4
- package/THREAT_MODEL.md +14 -0
- package/dist/cli/install/pre-push.js +3 -0
- package/dist/gateway/downstream.d.ts +16 -8
- package/dist/gateway/downstream.js +57 -11
- package/dist/gateway/meta/health.d.ts +77 -0
- package/dist/gateway/meta/health.js +160 -0
- package/dist/gateway/server.js +49 -8
- package/dist/policy/loader.d.ts +27 -0
- package/dist/policy/loader.js +15 -0
- package/dist/policy/types.d.ts +28 -0
- package/hooks/_lib/push-review-core.sh +1013 -0
- package/hooks/commit-review-gate.sh +51 -28
- package/hooks/push-review-gate-git.sh +92 -0
- package/hooks/push-review-gate.sh +47 -940
- package/package.json +1 -1
- package/scripts/dist-regression-gate.sh +220 -0
- package/scripts/tarball-smoke.sh +115 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dist-regression-gate.sh — class-level guard against "src/ changed, dist/ didn't"
|
|
3
|
+
#
|
|
4
|
+
# Generalizes BUG-013's trust-repair from a `[security]`-changeset-keyed gate
|
|
5
|
+
# (scripts/tarball-smoke.sh) to a gate that fires on ANY change set. Catches
|
|
6
|
+
# the 0.6.0 → 0.6.1 regression class: a release that ships dist/ byte-identical
|
|
7
|
+
# to the previous release despite src/ edits (i.e. dist/ was not rebuilt from
|
|
8
|
+
# the shipping commit).
|
|
9
|
+
#
|
|
10
|
+
# Bypass-resistant by construction:
|
|
11
|
+
# - Does not depend on changeset labels. A changeset-free PR that touches
|
|
12
|
+
# src/ without rebuilding dist/ still fails.
|
|
13
|
+
# - Does not depend on the release.yml rebuild step. That step is
|
|
14
|
+
# defense-in-depth at publish time; this gate fires on every PR and every
|
|
15
|
+
# push:main, so the regression is caught BEFORE it reaches the release
|
|
16
|
+
# branch.
|
|
17
|
+
#
|
|
18
|
+
# ## Algorithm
|
|
19
|
+
#
|
|
20
|
+
# 1. Read last-published version from `npm view @bookedsolid/rea version`.
|
|
21
|
+
# 2. Resolve the matching `v<version>` git tag. (Tag scheme verified across
|
|
22
|
+
# v0.1.0 … v0.6.2.)
|
|
23
|
+
# 3. Diff `src/` between HEAD and the tag. If unchanged, exit 0 — nothing
|
|
24
|
+
# to verify.
|
|
25
|
+
# 4. `npm pack @bookedsolid/rea@<version>` in a tempdir, hash the dist/
|
|
26
|
+
# tree in the extracted tarball.
|
|
27
|
+
# 5. Hash the local `dist/` tree the same way (assumes `pnpm build` has
|
|
28
|
+
# already run — CI enforces this, local runs need to pre-build).
|
|
29
|
+
# 6. If hashes are equal AND src/ changed → FAIL.
|
|
30
|
+
#
|
|
31
|
+
# ## Hash scheme
|
|
32
|
+
#
|
|
33
|
+
# `find dist -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256`
|
|
34
|
+
# matches release.yml's own hash recipe (lines 82, 130) so this gate and the
|
|
35
|
+
# post-publish verify step use the same digest. Per-file content sort, no
|
|
36
|
+
# mtime/permission bits — matches the logical-equality question we're asking.
|
|
37
|
+
#
|
|
38
|
+
# ## Exit codes
|
|
39
|
+
#
|
|
40
|
+
# 0 — pass, or skip (see "Skip surface" below)
|
|
41
|
+
# 1 — preflight failure: required tool missing from PATH, or dist/ directory
|
|
42
|
+
# absent at script start (run `pnpm build` first)
|
|
43
|
+
# 2 — REGRESSION — src/ changed vs last release but dist/ hash identical
|
|
44
|
+
#
|
|
45
|
+
# ## Skip surface (exit 0 without running the full check)
|
|
46
|
+
#
|
|
47
|
+
# The gate degrades to a clean skip on infrastructure failures so a transient
|
|
48
|
+
# registry outage or a malformed prior release does not pin every PR and
|
|
49
|
+
# every push:main run into red. All skip branches log a specific reason:
|
|
50
|
+
#
|
|
51
|
+
# - npm view found no published version → first-ever release, or registry
|
|
52
|
+
# outage at lookup time
|
|
53
|
+
# - matching git tag `v<version>` is not reachable and cannot be fetched
|
|
54
|
+
# from origin
|
|
55
|
+
# - src/ is byte-identical between HEAD and the tag → nothing to verify
|
|
56
|
+
# - npm pack against the previous version fails → registry outage, auth
|
|
57
|
+
# trouble, or the tarball was unpublished
|
|
58
|
+
# - npm pack succeeds but produces no `.tgz` → malformed pack output
|
|
59
|
+
# (rare; guarded separately from the pack-failure branch so the log
|
|
60
|
+
# reason stays specific)
|
|
61
|
+
# - the fetched tarball has no `package/dist/` → the baseline itself is
|
|
62
|
+
# broken; holding the next PR hostage to a bad prior release hurts more
|
|
63
|
+
# than it helps
|
|
64
|
+
#
|
|
65
|
+
# The release.yml rebuild-from-HEAD + post-publish tarball hash verification
|
|
66
|
+
# steps (see .github/workflows/release.yml lines 78-138) are the publish-time
|
|
67
|
+
# catching net that covers any case this gate skips. That layered defense is
|
|
68
|
+
# the point: this gate closes the common BUG-013 class on PR + push:main;
|
|
69
|
+
# the release workflow closes the rest at the moment it matters most.
|
|
70
|
+
#
|
|
71
|
+
# ## False-positive surface (known, documented)
|
|
72
|
+
#
|
|
73
|
+
# A whitespace-only edit in src/ that tsc compiles to byte-identical output
|
|
74
|
+
# WILL fail this gate. The failure message tells the committer to either:
|
|
75
|
+
# (a) include a meaningful src change whose dist artifact differs, or
|
|
76
|
+
# (b) `rm -rf dist && pnpm build && git add dist` to refresh timestamps
|
|
77
|
+
# inside dist — except the gate hashes content, not mtime, so (b) alone
|
|
78
|
+
# won't lift the failure.
|
|
79
|
+
#
|
|
80
|
+
# Adding an `[allow-noop-dist]` bypass marker was considered and rejected —
|
|
81
|
+
# it would re-open the BUG-013 attack surface for anyone who can open a PR.
|
|
82
|
+
# If you hit a legitimate noop-dist case, solve it by not committing the
|
|
83
|
+
# whitespace-only src change in isolation, or by combining it with a dist-
|
|
84
|
+
# affecting change in the same PR.
|
|
85
|
+
#
|
|
86
|
+
# ## Local usage
|
|
87
|
+
#
|
|
88
|
+
# pnpm build && scripts/dist-regression-gate.sh
|
|
89
|
+
#
|
|
90
|
+
# ## CI wiring
|
|
91
|
+
#
|
|
92
|
+
# Runs as its own ci.yml job after build. See .github/workflows/ci.yml
|
|
93
|
+
# `dist-regression` job for wiring.
|
|
94
|
+
|
|
95
|
+
set -euo pipefail
|
|
96
|
+
|
|
97
|
+
REPO_ROOT="$(cd -- "$(dirname -- "$0")/.." && pwd -P)"
|
|
98
|
+
cd "$REPO_ROOT"
|
|
99
|
+
|
|
100
|
+
log() { printf '[dist-regression] %s\n' "$*"; }
|
|
101
|
+
err() { printf '[dist-regression] %s\n' "$*" >&2; }
|
|
102
|
+
|
|
103
|
+
# Preflight — need npm, jq, git, shasum, tar.
|
|
104
|
+
for tool in npm jq git shasum tar; do
|
|
105
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
106
|
+
err "FAIL — required tool not on PATH: $tool"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
if [ ! -d "dist" ]; then
|
|
112
|
+
err "FAIL — dist/ not found. Run 'pnpm build' first."
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Resolve last published version from npm. Use --silent to suppress npm's
|
|
117
|
+
# own progress chatter; swallow stderr because we want a clean skip on a
|
|
118
|
+
# network error rather than a hard failure blocking every CI run.
|
|
119
|
+
PKG_NAME="$(jq -r '.name' package.json)"
|
|
120
|
+
PREV_VERSION="$(npm view "$PKG_NAME" version 2>/dev/null || true)"
|
|
121
|
+
if [ -z "$PREV_VERSION" ]; then
|
|
122
|
+
log "skip — no previous published version found for $PKG_NAME (network issue or first release)"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
log "last published: $PKG_NAME@$PREV_VERSION"
|
|
126
|
+
|
|
127
|
+
# Resolve the matching git tag. In CI, actions/checkout fetches tags only
|
|
128
|
+
# when fetch-depth: 0 (see ci.yml). Locally, the tag should exist. If the
|
|
129
|
+
# tag isn't reachable, fetch it from origin; if the fetch fails (offline,
|
|
130
|
+
# first-ever release), skip — this is the same safety valve as the
|
|
131
|
+
# "no previous published version" branch.
|
|
132
|
+
PREV_TAG="v$PREV_VERSION"
|
|
133
|
+
if ! git rev-parse --verify --quiet "$PREV_TAG" >/dev/null 2>&1; then
|
|
134
|
+
# Try a shallow fetch of just that tag. Redirect stderr because a missing
|
|
135
|
+
# tag on origin is expected behaviour for a brand-new package; we degrade
|
|
136
|
+
# to "skip" rather than "fail".
|
|
137
|
+
if ! git fetch --quiet --depth=1 origin "refs/tags/${PREV_TAG}:refs/tags/${PREV_TAG}" 2>/dev/null; then
|
|
138
|
+
log "skip — tag $PREV_TAG not reachable (offline CI or tag pruned)"
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
log "anchor tag: $PREV_TAG ($(git rev-parse --short "$PREV_TAG"))"
|
|
143
|
+
|
|
144
|
+
# Compare src/ trees. We use `git diff --name-only` so renames and mode-
|
|
145
|
+
# only changes count. If src/ is unchanged vs the tag, the dist/ gate has
|
|
146
|
+
# nothing to verify — a PR that touches only docs/hooks/CI should not fail
|
|
147
|
+
# this check.
|
|
148
|
+
SRC_CHANGED_COUNT="$(git diff --name-only "$PREV_TAG" HEAD -- src/ | wc -l | awk '{print $1}')"
|
|
149
|
+
if [ "$SRC_CHANGED_COUNT" -eq 0 ]; then
|
|
150
|
+
log "skip — src/ unchanged since $PREV_TAG"
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
log "src/ changes vs $PREV_TAG: $SRC_CHANGED_COUNT file(s)"
|
|
154
|
+
|
|
155
|
+
# Fetch the published tarball and compute its dist/ hash. Using `npm pack`
|
|
156
|
+
# against the published name is more stable than scraping the registry URL
|
|
157
|
+
# because npm handles auth + CDN redirects. --silent keeps the only stdout
|
|
158
|
+
# noise to the tarball filename.
|
|
159
|
+
WORK="$(mktemp -d -t rea-dist-regression-XXXXXX)"
|
|
160
|
+
trap 'rm -rf -- "$WORK"' EXIT HUP INT TERM
|
|
161
|
+
|
|
162
|
+
# Degrade-to-skip on infrastructure failures. `npm view` above already
|
|
163
|
+
# skips on network errors; `npm pack` and tarball-shape checks need to
|
|
164
|
+
# match, otherwise a registry outage or a broken prior release (e.g. if
|
|
165
|
+
# the shipping 0.6.1 tarball itself had been malformed rather than merely
|
|
166
|
+
# stale) would pin every PR run into red until the registry recovered or
|
|
167
|
+
# a new tarball was published. The release.yml rebuild+verify step
|
|
168
|
+
# remains the catching net at publish time, so skipping here does not
|
|
169
|
+
# re-open the BUG-013 attack surface for the merge-to-main path.
|
|
170
|
+
if ! ( cd "$WORK" && npm pack "${PKG_NAME}@${PREV_VERSION}" --silent >/dev/null 2>&1 ); then
|
|
171
|
+
log "skip — npm pack ${PKG_NAME}@${PREV_VERSION} failed (network issue or registry outage)"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
TARBALL="$(find "$WORK" -maxdepth 1 -type f -name '*.tgz' | head -1)"
|
|
176
|
+
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
|
|
177
|
+
log "skip — npm pack produced no tarball for ${PKG_NAME}@${PREV_VERSION} in $WORK"
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
mkdir -p "$WORK/extract"
|
|
182
|
+
tar -xzf "$TARBALL" -C "$WORK/extract"
|
|
183
|
+
|
|
184
|
+
if [ ! -d "$WORK/extract/package/dist" ]; then
|
|
185
|
+
log "skip — published tarball for $PREV_VERSION has no dist/ at package/dist (broken baseline)"
|
|
186
|
+
exit 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
hash_tree() {
|
|
190
|
+
# $1 — directory holding `dist/`
|
|
191
|
+
# Match the recipe from .github/workflows/release.yml:82,130 exactly so
|
|
192
|
+
# this gate and the release-time verify step speak the same digest.
|
|
193
|
+
( cd "$1" && find dist -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | awk '{print $1}' )
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
PUBLISHED_HASH="$(hash_tree "$WORK/extract/package")"
|
|
197
|
+
CURRENT_HASH="$(hash_tree "$REPO_ROOT")"
|
|
198
|
+
|
|
199
|
+
log "published ($PREV_VERSION) dist/ hash: $PUBLISHED_HASH"
|
|
200
|
+
log "current dist/ hash: $CURRENT_HASH"
|
|
201
|
+
|
|
202
|
+
if [ "$PUBLISHED_HASH" = "$CURRENT_HASH" ]; then
|
|
203
|
+
err ""
|
|
204
|
+
err "FAIL — REGRESSION: src/ has $SRC_CHANGED_COUNT file change(s) vs $PREV_TAG"
|
|
205
|
+
err " but current dist/ hash is byte-identical to the published tarball."
|
|
206
|
+
err ""
|
|
207
|
+
err " This is the 0.6.0 → 0.6.1 regression class (BUG-013): dist/ was"
|
|
208
|
+
err " not rebuilt from HEAD. Running a fresh build should refresh the"
|
|
209
|
+
err " compiled output; if it does not, one or more src/ changes are"
|
|
210
|
+
err " whitespace-only and produce no dist/ delta — in that case,"
|
|
211
|
+
err " batch them with a change that DOES affect compiled output."
|
|
212
|
+
err ""
|
|
213
|
+
err " To diagnose locally:"
|
|
214
|
+
err " rm -rf dist && pnpm build"
|
|
215
|
+
err " scripts/dist-regression-gate.sh"
|
|
216
|
+
err ""
|
|
217
|
+
exit 2
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
log "PASS — dist/ differs from $PREV_TAG baseline, as expected."
|
package/scripts/tarball-smoke.sh
CHANGED
|
@@ -181,6 +181,121 @@ echo "[smoke] → $AGENT_COUNT agents, $HOOK_COUNT hooks, $COMMAND_COUNT comma
|
|
|
181
181
|
echo "[smoke] rea doctor"
|
|
182
182
|
./node_modules/.bin/rea doctor
|
|
183
183
|
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# BUG-013 — security-claim content gate.
|
|
186
|
+
#
|
|
187
|
+
# If any changeset carries the `[security]` marker, the tarball MUST ship
|
|
188
|
+
# compiled evidence of the claimed fix. The rule:
|
|
189
|
+
#
|
|
190
|
+
# 1. Find every `.changeset/*.md` in the source tree that contains `[security]`
|
|
191
|
+
# 2. Assert AT LEAST ONE `*sanitize*.test.ts` or `*security*.test.ts` exists
|
|
192
|
+
# under `src/` (a "security-claim" changeset without a matching regression
|
|
193
|
+
# test is a marketing bullet, not a shipped fix)
|
|
194
|
+
# 3. For every such test file, extract the symbols it imports from the
|
|
195
|
+
# module under test (named imports from relative paths) and assert each
|
|
196
|
+
# symbol appears somewhere under `dist/`. Tests are excluded from the
|
|
197
|
+
# npm build (tsconfig.build.json), so a stale dist/ from a prior release
|
|
198
|
+
# would not contain the new symbol that the test exercises — this catches
|
|
199
|
+
# the 0.6.0→0.6.1 byte-identical dist/ regression that motivated BUG-013.
|
|
200
|
+
#
|
|
201
|
+
# Bypass-resistant: the gate keys on the changeset marker, not a flag the
|
|
202
|
+
# release author chooses. Narrow: no-op when no `[security]` changesets exist.
|
|
203
|
+
#
|
|
204
|
+
# Known limits (called out honestly rather than papered over):
|
|
205
|
+
# - The gate asserts the imported SYMBOLS are present in dist/. It does
|
|
206
|
+
# NOT assert those symbols are NEW vs. the previous published release.
|
|
207
|
+
# A test that imports only pre-existing symbols would satisfy the gate
|
|
208
|
+
# against a stale dist/. The two defense-in-depth layers that close
|
|
209
|
+
# this gap — `Rebuild dist/ from HEAD before publish` and
|
|
210
|
+
# `Verify published tarball dist/ matches CI-built dist/` — live in
|
|
211
|
+
# `.github/workflows/release.yml` (see `.rea/drafts-0.6.2/` for the
|
|
212
|
+
# pending hand-apply patch). The content gate here catches the
|
|
213
|
+
# 0.6.0→0.6.1 class of regression in the common case; the workflow
|
|
214
|
+
# hash check catches the adversarial case.
|
|
215
|
+
# - The gate does not tie a specific changeset to a specific test file.
|
|
216
|
+
# If a security changeset names BUG-X but the shipping security test
|
|
217
|
+
# covers BUG-Y, the gate passes. Mitigation is the same: the workflow
|
|
218
|
+
# hash verification plus human review of the changeset at PR time.
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
SEC_CHANGESETS="$(grep -l '\[security\]' "$REPO_ROOT"/.changeset/*.md 2>/dev/null || true)"
|
|
221
|
+
if [ -n "$SEC_CHANGESETS" ]; then
|
|
222
|
+
echo "[smoke] security-claim gate: $(printf '%s\n' "$SEC_CHANGESETS" | wc -l | awk '{print $1}') changeset(s) tagged [security]"
|
|
223
|
+
|
|
224
|
+
SEC_SRC_TESTS="$(cd "$REPO_ROOT" && find src -type f \( -name '*sanitize*.test.ts' -o -name '*security*.test.ts' \) 2>/dev/null | sort)"
|
|
225
|
+
if [ -z "$SEC_SRC_TESTS" ]; then
|
|
226
|
+
echo "[smoke] FAIL — [security] changeset present but no *sanitize*.test.ts or *security*.test.ts under src/" >&2
|
|
227
|
+
echo "[smoke] a security-claim changeset with no matching regression test is a trust violation" >&2
|
|
228
|
+
exit 2
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# For each security test, collect the named imports pulled from relative
|
|
232
|
+
# paths — those are the symbols under test and must be compiled into dist/.
|
|
233
|
+
# Example line we want to match:
|
|
234
|
+
# import { sanitizeHealthSnapshot, INJECTION_REDACTED_PLACEHOLDER } from './health';
|
|
235
|
+
# We ignore imports from bare package names ('vitest', 'node:fs', etc.).
|
|
236
|
+
MISSING_SYMBOLS=""
|
|
237
|
+
SYMBOL_COUNT=0
|
|
238
|
+
while IFS= read -r src_test; do
|
|
239
|
+
[ -z "$src_test" ] && continue
|
|
240
|
+
# Collect named imports from relative-path sources using perl for a
|
|
241
|
+
# multi-line regex. Output: one symbol per line.
|
|
242
|
+
# We intentionally skip:
|
|
243
|
+
# - `import type { ... }` — entire clause is type-only
|
|
244
|
+
# - `{ ..., type Foo, ... }` — inline type-only marker on a member
|
|
245
|
+
# TypeScript erases both at compile time, so asserting them against dist/
|
|
246
|
+
# would false-positive. Also skip `as` aliases (the aliased symbol is a
|
|
247
|
+
# local rebind, not the exported one we want to grep).
|
|
248
|
+
SYMBOLS="$(perl -0777 -ne '
|
|
249
|
+
while (/import(\s+type)?\s*\{([^}]+)\}\s*from\s*[\x27"](\.[^\x27"]+)[\x27"]/sg) {
|
|
250
|
+
next if $1; # whole clause is `import type { ... }` — skip
|
|
251
|
+
my $group = $2;
|
|
252
|
+
$group =~ s/\s+/ /g;
|
|
253
|
+
for my $sym (split /,/, $group) {
|
|
254
|
+
$sym =~ s/^\s+|\s+$//g;
|
|
255
|
+
next if $sym =~ /^type\s+/; # inline `type Foo` — skip
|
|
256
|
+
$sym =~ s/\s+as\s+\w+$//;
|
|
257
|
+
next unless $sym =~ /^\w+$/;
|
|
258
|
+
print "$sym\n";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
' "$REPO_ROOT/$src_test" | sort -u)"
|
|
262
|
+
|
|
263
|
+
while IFS= read -r sym; do
|
|
264
|
+
[ -z "$sym" ] && continue
|
|
265
|
+
SYMBOL_COUNT=$((SYMBOL_COUNT + 1))
|
|
266
|
+
# grep -r across dist/ — if the symbol does not appear anywhere, the
|
|
267
|
+
# build did not include the fix the test covers.
|
|
268
|
+
if ! grep -r --include='*.js' -l -F -w "$sym" "$REPO_ROOT/dist" >/dev/null 2>&1; then
|
|
269
|
+
MISSING_SYMBOLS="$MISSING_SYMBOLS
|
|
270
|
+
$sym (imported by $src_test)"
|
|
271
|
+
fi
|
|
272
|
+
done <<< "$SYMBOLS"
|
|
273
|
+
done <<< "$SEC_SRC_TESTS"
|
|
274
|
+
|
|
275
|
+
if [ -n "$MISSING_SYMBOLS" ]; then
|
|
276
|
+
echo "[smoke] FAIL — [security] changeset present but symbols under test are MISSING from dist/:" >&2
|
|
277
|
+
echo "[smoke] (dist/ may be stale — rebuild before publishing)" >&2
|
|
278
|
+
printf '%s\n' "$MISSING_SYMBOLS" >&2
|
|
279
|
+
exit 2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Codex review blocker #1 (2026-04-20) — a test file written with
|
|
283
|
+
# namespace/default/dynamic imports, or one that only imports from bare
|
|
284
|
+
# packages, produces zero symbols to check. Before this guard, the gate
|
|
285
|
+
# would pass with "0 symbols all present in dist/", re-opening the
|
|
286
|
+
# byte-identical-dist/ regression that BUG-013 was written to catch.
|
|
287
|
+
if [ "$SYMBOL_COUNT" -eq 0 ]; then
|
|
288
|
+
echo "[smoke] FAIL — [security] changeset present but no checkable symbols extracted" >&2
|
|
289
|
+
echo "[smoke] one or more src/**/(*sanitize*|*security*).test.ts files must use" >&2
|
|
290
|
+
echo "[smoke] the \`import { Named } from './relative'\` shape so the gate can" >&2
|
|
291
|
+
echo "[smoke] verify the symbol under test appears in compiled dist/." >&2
|
|
292
|
+
echo "[smoke] (namespace/default/dynamic-only imports can't be verified)" >&2
|
|
293
|
+
exit 2
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
echo "[smoke] → $(printf '%s\n' "$SEC_SRC_TESTS" | wc -l | awk '{print $1}') security regression test(s), $SYMBOL_COUNT imported symbol(s) all present in dist/"
|
|
297
|
+
fi
|
|
298
|
+
|
|
184
299
|
# Verify every declared public export resolves. If the exports map points at a
|
|
185
300
|
# file that didn't ship in `files:`, this is where we catch it.
|
|
186
301
|
echo "[smoke] resolve exports"
|