mandrel 2.29.0 → 2.30.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.
@@ -106,6 +106,73 @@ function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
106
106
  * rows: Array<{file: string, method: string, startLine: number, crap: number}>,
107
107
  * }|null}
108
108
  */
109
+ /**
110
+ * The envelope-level fields the CRAP compat axes read off a *loaded* baseline
111
+ * — the set every read path owes `assertBaselineCompatible`.
112
+ *
113
+ * It exists because there are TWO read paths and they have now diverged three
114
+ * times. `check-baselines` loads through `baselines/reader.js`; the
115
+ * `quality-preview` pre-commit arm loads through `projectCrapEnvelopeToLegacy`
116
+ * below. Both are ALLOW-LISTS, both feed the same axes, and a stamp added to
117
+ * one and not the other yields two opposite verdicts on one file: Story #4866
118
+ * (`scoringSemantics`, `tsTranspilerVersion`), Story #4969 (`rows[].anonymous`)
119
+ * and Story #4986 (`provenanceStamped`, half-fixed by #4973) were each that
120
+ * same half-landing.
121
+ *
122
+ * Every one of those axes keys on a POSITIVE marker, so a dropped field reads
123
+ * `undefined` and fails the baseline closed with a remedy that cannot work —
124
+ * re-deriving it writes the stamp the read path then discards. The projection
125
+ * below is DERIVED from this list rather than repeating it, so a new stamp is
126
+ * carried here the moment it is named; a parity test holds the reader to the
127
+ * same set and names whichever path forgot one.
128
+ *
129
+ * Row-level markers are deliberately out: they are projected per-row, not as
130
+ * envelope stamps.
131
+ *
132
+ * Deliberately module-local, mirroring `SCORING_SEMANTICS` in
133
+ * `baselines/kinds/crap.js`: the writer's `envelopeExtras()` is the single
134
+ * production door to the stamp set, and exporting this list would add a second
135
+ * one that only a test reaches. The parity test holds this projection to
136
+ * `Object.keys(envelopeExtras())` instead, so a stamp the writer starts
137
+ * emitting fails the test here until it is named — which is the enforcement
138
+ * this list needs, not an export.
139
+ */
140
+ const COMPAT_STAMP_FIELDS = Object.freeze([
141
+ 'scoringSemantics',
142
+ 'tsTranspilerVersion',
143
+ 'provenanceStamped',
144
+ ]);
145
+
146
+ /**
147
+ * Per-stamp coercion applied on the way through the legacy projection. A field
148
+ * with no entry is carried VERBATIM, which is the correct default: the axes
149
+ * distinguish "stamped" from "absent", so inventing a value for a stamp the
150
+ * envelope never wrote is the one thing a read path must not do.
151
+ */
152
+ const COMPAT_STAMP_NORMALIZERS = {
153
+ // `null` (not the running value) when unstamped, so `ts-transpiler-drift`
154
+ // can tell "written by a different transpiler" from "written before the
155
+ // stamp existed" instead of comparing a value against itself.
156
+ tsTranspilerVersion: (value) => (typeof value === 'string' ? value : null),
157
+ scoringSemantics: (value) => value ?? null,
158
+ };
159
+
160
+ /**
161
+ * Project the compat stamps off a v2 envelope, driven by
162
+ * `COMPAT_STAMP_FIELDS`.
163
+ *
164
+ * @param {Record<string, unknown>} parsed
165
+ * @returns {Record<string, unknown>}
166
+ */
167
+ function projectCompatStamps(parsed) {
168
+ const stamps = {};
169
+ for (const field of COMPAT_STAMP_FIELDS) {
170
+ const normalize = COMPAT_STAMP_NORMALIZERS[field];
171
+ stamps[field] = normalize ? normalize(parsed[field]) : parsed[field];
172
+ }
173
+ return stamps;
174
+ }
175
+
109
176
  /**
110
177
  * Story #1895: shipped baseline switched to the canonical envelope shape
111
178
  * (`$schema`, `kernelVersion`, `generatedAt`, `rollup`, `rows` keyed on
@@ -122,6 +189,16 @@ function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
122
189
  * They are carried verbatim now, `null` when the envelope never stamped them,
123
190
  * so an axis can tell "written by a different transpiler" apart from "written
124
191
  * before the stamp existed" instead of guessing.
192
+ *
193
+ * **This projection is one of TWO (Story #4986).** `check-baselines` reads a
194
+ * baseline through `baselines/reader.js`; `quality-preview` reads the same file
195
+ * through here. Both feed `assertBaselineCompatible`, so a stamp added to one
196
+ * allow-list and not the other produces two opposite verdicts on one envelope —
197
+ * which is what happened to `provenanceStamped`: #4973 added it to the reader
198
+ * and left this projection dropping it, so the pre-commit CRAP arm rejected
199
+ * every stamped baseline with an un-satisfiable "re-seed" remedy while the
200
+ * authoritative gate passed. The stamp block is derived from
201
+ * `COMPAT_STAMP_FIELDS` above so this projection cannot fall behind again.
125
202
  */
126
203
  function projectCrapEnvelopeToLegacy(parsed) {
127
204
  if (
@@ -134,11 +211,7 @@ function projectCrapEnvelopeToLegacy(parsed) {
134
211
  return {
135
212
  kernelVersion: parsed.kernelVersion,
136
213
  escomplexVersion: resolveEscomplexVersion(),
137
- tsTranspilerVersion:
138
- typeof parsed.tsTranspilerVersion === 'string'
139
- ? parsed.tsTranspilerVersion
140
- : null,
141
- scoringSemantics: parsed.scoringSemantics ?? null,
214
+ ...projectCompatStamps(parsed),
142
215
  rows: parsed.rows.map((row) => ({
143
216
  crap: row.crap,
144
217
  file: row.path,
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,13 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.30.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.29.0...mandrel-v2.30.0) (2026-08-04)
19
+
20
+
21
+ ### Fixed
22
+
23
+ * **baselines:** carry provenanceStamped through the legacy CRAP projection ([#4986](https://github.com/dsj1984/mandrel/issues/4986)) ([#4987](https://github.com/dsj1984/mandrel/issues/4987)) ([921b521](https://github.com/dsj1984/mandrel/commit/921b521f2094c961fec84acca1f90e068cb77798))
24
+
18
25
  ## [2.29.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.28.0...mandrel-v2.29.0) (2026-08-03)
19
26
 
20
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.29.0",
3
+ "version": "2.30.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",