mandrel 2.26.0 → 2.27.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.
@@ -182,6 +182,56 @@ function validate(kind, parsed, sourceHint) {
182
182
  }
183
183
  }
184
184
 
185
+ /**
186
+ * Internal: the ONE narrowing projection every loaded envelope passes through.
187
+ *
188
+ * `load` and `loadFile` reach a validated `parsed` envelope by different routes
189
+ * — one resolves the path from config, the other infers the kind from
190
+ * `$schema` — but the shape they hand back is the same narrow contract, so it
191
+ * is built here rather than written out at each exit.
192
+ *
193
+ * **That single-sourcing is the point, not tidiness.** The projection is an
194
+ * ALLOW-LIST: a field absent from it is silently dropped, and the envelope
195
+ * stamps below are read off the LOADED object by compat axes that fail closed
196
+ * when a stamp reads `undefined`. While the list was duplicated at the two
197
+ * exits, adding a stamp to one and not the other produced a gate that rejected
198
+ * every baseline in the repo for a stamp that was present on disk — which is
199
+ * exactly what happened to `provenanceStamped` between Story #4901 and this
200
+ * fix. One list means a new stamp cannot be half-added.
201
+ *
202
+ * @param {string} kind
203
+ * @param {object} parsed A validated baseline envelope.
204
+ * @returns {{ rollup: object, rows: Array<object>, kernelVersion: string, generatedAt: string }}
205
+ */
206
+ function shapeEnvelope(kind, parsed) {
207
+ const rows = Array.isArray(parsed.rows)
208
+ ? parsed.rows.map((row) => canonicaliseRow(kind, row))
209
+ : [];
210
+ return {
211
+ rollup: parsed.rollup ?? { '*': {} },
212
+ rows,
213
+ kernelVersion: parsed.kernelVersion,
214
+ generatedAt: parsed.generatedAt,
215
+ // Story #4775 — carry the per-kind scoring-semantics stamp through the
216
+ // narrowing. The gate's compat check reads it off the LOADED envelope, so
217
+ // dropping it here would make every baseline look unstamped and fail the
218
+ // whole repo closed on a stamp that is actually present on disk.
219
+ scoringSemantics: parsed.scoringSemantics,
220
+ // Story #4866 — same contract for the transpiler stamp. Dropping it here
221
+ // would leave the ts-transpiler compat axis reachable but blind: it would
222
+ // read `undefined` off every loaded envelope and pass vacuously, which is
223
+ // the exact deadness this Story exists to end.
224
+ tsTranspilerVersion: parsed.tsTranspilerVersion,
225
+ // Story #4901 — the coordinate-provenance marker, and the stamp that proved
226
+ // the warnings above were not hypothetical. Its `provenance-unstamped` axis
227
+ // keys on `!== true`, so while this line was missing the axis read
228
+ // `undefined` off every envelope and rejected each one with "baseline
229
+ // predates coordinate-provenance stamping" — un-satisfiable, because
230
+ // re-deriving the baseline writes the marker the reader then drops.
231
+ provenanceStamped: parsed.provenanceStamped,
232
+ };
233
+ }
234
+
185
235
  /**
186
236
  * Internal: parse + validate + canonicalise. Used by both `load` and
187
237
  * `loadFile`.
@@ -208,25 +258,7 @@ function readAndShape(kind, absolutePath) {
208
258
  );
209
259
  }
210
260
  validate(kind, parsed, absolutePath);
211
- const rows = Array.isArray(parsed.rows)
212
- ? parsed.rows.map((row) => canonicaliseRow(kind, row))
213
- : [];
214
- return {
215
- rollup: parsed.rollup ?? { '*': {} },
216
- rows,
217
- kernelVersion: parsed.kernelVersion,
218
- generatedAt: parsed.generatedAt,
219
- // Story #4775 — carry the per-kind scoring-semantics stamp through the
220
- // narrowing. The gate's compat check reads it off the LOADED envelope, so
221
- // dropping it here would make every baseline look unstamped and fail the
222
- // whole repo closed on a stamp that is actually present on disk.
223
- scoringSemantics: parsed.scoringSemantics,
224
- // Story #4866 — same contract for the transpiler stamp. Dropping it here
225
- // would leave the ts-transpiler compat axis reachable but blind: it would
226
- // read `undefined` off every loaded envelope and pass vacuously, which is
227
- // the exact deadness this Story exists to end.
228
- tsTranspilerVersion: parsed.tsTranspilerVersion,
229
- };
261
+ return shapeEnvelope(kind, parsed);
230
262
  }
231
263
 
232
264
  /**
@@ -306,25 +338,7 @@ export function loadFile(absolutePath, opts = {}) {
306
338
  );
307
339
  }
308
340
  validate(kind, parsed, absolutePath);
309
- const rows = Array.isArray(parsed.rows)
310
- ? parsed.rows.map((row) => canonicaliseRow(kind, row))
311
- : [];
312
- return {
313
- rollup: parsed.rollup ?? { '*': {} },
314
- rows,
315
- kernelVersion: parsed.kernelVersion,
316
- generatedAt: parsed.generatedAt,
317
- // Story #4775 — carry the per-kind scoring-semantics stamp through the
318
- // narrowing. The gate's compat check reads it off the LOADED envelope, so
319
- // dropping it here would make every baseline look unstamped and fail the
320
- // whole repo closed on a stamp that is actually present on disk.
321
- scoringSemantics: parsed.scoringSemantics,
322
- // Story #4866 — same contract for the transpiler stamp. Dropping it here
323
- // would leave the ts-transpiler compat axis reachable but blind: it would
324
- // read `undefined` off every loaded envelope and pass vacuously, which is
325
- // the exact deadness this Story exists to end.
326
- tsTranspilerVersion: parsed.tsTranspilerVersion,
327
- };
341
+ return shapeEnvelope(kind, parsed);
328
342
  }
329
343
 
330
344
  export const _internals = Object.freeze({
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.27.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.26.0...mandrel-v2.27.0) (2026-08-03)
19
+
20
+
21
+ ### Fixed
22
+
23
+ * **baselines:** carry provenanceStamped through the reader's narrowing (refs [#4973](https://github.com/dsj1984/mandrel/issues/4973)) ([#4974](https://github.com/dsj1984/mandrel/issues/4974)) ([4a3795a](https://github.com/dsj1984/mandrel/commit/4a3795a2e3d60748d5b40b0288521af5a7a885ab))
24
+
18
25
  ## [2.26.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.25.0...mandrel-v2.26.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.26.0",
3
+ "version": "2.27.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/",