@firestartr/cli 2.11.0-snapshot-patch-claims → 2.11.0-snapshot-patch-claims-2

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.
Files changed (2) hide show
  1. package/build/index.js +94 -50
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -166237,7 +166237,7 @@ function isRecord(value) {
166237
166237
  // normalized pointer. Array appends (`add ... /-`) collide on the pointer plus
166238
166238
  // the appended element's `name` when present, so several features can each add
166239
166239
  // a distinct label to /providers/github/labels/- while the same label name from
166240
- // two features still hard-fails.
166240
+ // two features is silently deduplicated (or errors if values differ).
166241
166241
  function writeIdentity(patch) {
166242
166242
  const path = normalizePointer(patch.path);
166243
166243
  if (patch.op === 'add' &&
@@ -166248,6 +166248,30 @@ function writeIdentity(patch) {
166248
166248
  }
166249
166249
  return path;
166250
166250
  }
166251
+ // Check whether an array at `appendPath`'s parent already contains an element
166252
+ // whose `name` matches `value.name`. Used for the "claim wins" gate: when a
166253
+ // feature tries to append an array element that already exists in the claim,
166254
+ // the patch is silently dropped.
166255
+ function arrayElementExists(doc, appendPath, value) {
166256
+ if (!isRecord(value) || typeof value.name !== 'string')
166257
+ return false;
166258
+ const parentPath = appendPath.slice(0, -2); // strip trailing /-
166259
+ const segments = parentPath
166260
+ .split('/')
166261
+ .slice(1)
166262
+ .map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
166263
+ let cur = doc;
166264
+ for (const seg of segments) {
166265
+ if (cur === null || cur === undefined || typeof cur !== 'object')
166266
+ return false;
166267
+ if (!Object.prototype.hasOwnProperty.call(cur, seg))
166268
+ return false;
166269
+ cur = cur[seg];
166270
+ }
166271
+ if (!Array.isArray(cur))
166272
+ return false;
166273
+ return cur.some((el) => isRecord(el) && el.name === value.name);
166274
+ }
166251
166275
  // fast-json-patch does not create intermediate containers for `add`, and
166252
166276
  // appending to an array requires the array to already exist. Ensure every
166253
166277
  // ancestor segment exists before applying an `add`: missing containers are
@@ -166277,9 +166301,7 @@ async function stitchClaim(claimAfterDefaults, preCollectedEnvelopes) {
166277
166301
  if (envelopes.length === 0) {
166278
166302
  return claimAfterDefaults;
166279
166303
  }
166280
- const violations = [];
166281
- const seenPaths = new Map();
166282
- // Collect all patches for deduplication before any gate — normalize once so all gates and apply use same pointer
166304
+ // Normalize all patches once so every gate and the apply phase use the same pointers.
166283
166305
  const normalizedEnvelopes = envelopes.map((env) => ({
166284
166306
  feature: env.feature,
166285
166307
  patches: env.patches.map((p) => ({
@@ -166288,8 +166310,50 @@ async function stitchClaim(claimAfterDefaults, preCollectedEnvelopes) {
166288
166310
  _raw: p.path,
166289
166311
  })),
166290
166312
  }));
166291
- for (const envelope of normalizedEnvelopes) {
166292
- for (const patch of envelope.patches) {
166313
+ // ── Phase 1: silently drop patches that should not be applied ──────────
166314
+ // • Array append whose element already exists in the claim → claim wins
166315
+ // • Duplicate identity with identical value across features → silent dedup
166316
+ const dropped = new Set(); // "envelopeIdx:patchIdx"
166317
+ const seenIdentities = new Map();
166318
+ for (let ei = 0; ei < normalizedEnvelopes.length; ei++) {
166319
+ const envelope = normalizedEnvelopes[ei];
166320
+ for (let pi = 0; pi < envelope.patches.length; pi++) {
166321
+ const patch = envelope.patches[pi];
166322
+ const key = `${ei}:${pi}`;
166323
+ // Array claim-wins: element already in claim → drop
166324
+ if (patch.op === 'add' && isAppendPointer(patch.path)) {
166325
+ if (arrayElementExists(claimAfterDefaults, patch.path, patch.value)) {
166326
+ dropped.add(key);
166327
+ continue;
166328
+ }
166329
+ }
166330
+ // Silent dedup: same identity + same value → drop
166331
+ const identity = writeIdentity(patch);
166332
+ const existing = seenIdentities.get(identity);
166333
+ if (existing) {
166334
+ if (JSON.stringify(existing.value) === JSON.stringify(patch.value)) {
166335
+ dropped.add(key);
166336
+ continue;
166337
+ }
166338
+ // Different value — kept here, caught as a violation in Phase 2.
166339
+ }
166340
+ else {
166341
+ seenIdentities.set(identity, {
166342
+ feature: envelope.feature,
166343
+ value: patch.value,
166344
+ });
166345
+ }
166346
+ }
166347
+ }
166348
+ // ── Phase 2: collect violations on non-dropped patches ─────────────────
166349
+ const violations = [];
166350
+ const seenForViolations = new Map(); // identity → feature
166351
+ for (let ei = 0; ei < normalizedEnvelopes.length; ei++) {
166352
+ const envelope = normalizedEnvelopes[ei];
166353
+ for (let pi = 0; pi < envelope.patches.length; pi++) {
166354
+ if (dropped.has(`${ei}:${pi}`))
166355
+ continue;
166356
+ const patch = envelope.patches[pi];
166293
166357
  const path = patch.path;
166294
166358
  const raw = patch._raw ?? path;
166295
166359
  // Gate 1: protected path
@@ -166301,54 +166365,32 @@ async function stitchClaim(claimAfterDefaults, preCollectedEnvelopes) {
166301
166365
  reason: 'protected-path',
166302
166366
  message: `Feature '${envelope.feature}' patch ${patch.op} ${raw} (normalized ${path}) targets protected claim path`,
166303
166367
  });
166304
- // continue to collect other violations for same patch (but avoid duplicate counting)
166305
166368
  }
166306
- // Gate 3: deduplication — track before overwrite so we report both.
166307
- // Array appends (`/-`) dedup by element name (see writeIdentity); all
166308
- // other writes dedup by normalized pointer.
166369
+ // Gate 2: conflicting value from another feature
166309
166370
  const identity = writeIdentity(patch);
166310
- if (seenPaths.has(identity)) {
166371
+ if (seenForViolations.has(identity)) {
166311
166372
  violations.push({
166312
166373
  feature: envelope.feature,
166313
166374
  op: patch.op,
166314
166375
  path,
166315
166376
  reason: 'duplicate-path',
166316
- conflictingFeature: seenPaths.get(identity),
166317
- message: `Feature '${envelope.feature}' and '${seenPaths.get(identity)}' both write to ${path} (deduplication)`,
166377
+ conflictingFeature: seenForViolations.get(identity),
166378
+ message: `Feature '${envelope.feature}' and '${seenForViolations.get(identity)}' both write to ${path} (deduplication)`,
166318
166379
  });
166319
166380
  }
166320
166381
  else {
166321
- seenPaths.set(identity, envelope.feature);
166322
- }
166323
- // Gate 2: overwrite protection (II-a strict: raw ∪ defaults counts as declared)
166324
- if (['replace', 'remove', 'add'].includes(patch.op)) {
166325
- const exists = pointerExists(claimAfterDefaults, path);
166326
- if (exists) {
166327
- // For strict II-a, any op to existing path is overwrite
166328
- // We already reported protected if applicable; still report overwrite unless it was protected
166329
- // to keep collect-all but avoid double-counting same patch as two reasons? Keep both for visibility.
166330
- if (!isProtectedPath(path)) {
166331
- violations.push({
166332
- feature: envelope.feature,
166333
- op: patch.op,
166334
- path,
166335
- reason: 'overwrite-protected',
166336
- message: `Feature '${envelope.feature}' patch ${patch.op} ${raw} (normalized ${path}) would overwrite user/default-declared value`,
166337
- });
166338
- }
166339
- }
166340
- else {
166341
- // Path absent — check intermediate parent existence? For now exact path only.
166342
- // If patch is `replace` to absent path, fast-json-patch will fail; we let that be an overwrite concern too.
166343
- if (['replace', 'remove'].includes(patch.op)) {
166344
- violations.push({
166345
- feature: envelope.feature,
166346
- op: patch.op,
166347
- path,
166348
- reason: 'overwrite-protected',
166349
- message: `Feature '${envelope.feature}' patch ${patch.op} ${raw} (normalized ${path}) targets absent path (replace/remove requires existing value)`,
166350
- });
166351
- }
166382
+ seenForViolations.set(identity, envelope.feature);
166383
+ }
166384
+ // Gate 3: replace / remove to an absent path
166385
+ if (['replace', 'remove'].includes(patch.op)) {
166386
+ if (!pointerExists(claimAfterDefaults, path)) {
166387
+ violations.push({
166388
+ feature: envelope.feature,
166389
+ op: patch.op,
166390
+ path,
166391
+ reason: 'overwrite-protected',
166392
+ message: `Feature '${envelope.feature}' patch ${patch.op} ${raw} (normalized ${path}) targets absent path (replace/remove requires existing value)`,
166393
+ });
166352
166394
  }
166353
166395
  }
166354
166396
  }
@@ -166356,17 +166398,19 @@ async function stitchClaim(claimAfterDefaults, preCollectedEnvelopes) {
166356
166398
  if (violations.length > 0) {
166357
166399
  throw new StitchingError(violations);
166358
166400
  }
166359
- // No violations — apply patches sequentially (deduplication already ensures no conflicts)
166401
+ // ── Phase 3: apply remaining patches ───────────────────────────────────
166360
166402
  let stitched = JSON.parse(JSON.stringify(claimAfterDefaults));
166361
- for (const envelope of normalizedEnvelopes) {
166362
- if (envelope.patches.length === 0)
166403
+ for (let ei = 0; ei < normalizedEnvelopes.length; ei++) {
166404
+ const envelope = normalizedEnvelopes[ei];
166405
+ const patchesToApply = envelope.patches.filter((_, pi) => !dropped.has(`${ei}:${pi}`));
166406
+ if (patchesToApply.length === 0)
166363
166407
  continue;
166364
- for (const patch of envelope.patches) {
166408
+ for (const patch of patchesToApply) {
166365
166409
  if (patch.op === 'add') {
166366
166410
  stitched = ensureAddParents(stitched, patch);
166367
166411
  }
166368
166412
  }
166369
- const result = fast_json_patch.applyPatch(stitched, envelope.patches, false, false);
166413
+ const result = fast_json_patch.applyPatch(stitched, patchesToApply, false, false);
166370
166414
  stitched = result.newDocument;
166371
166415
  }
166372
166416
  return stitched;
@@ -713815,7 +713859,7 @@ const crsStatusSubcommand = {
713815
713859
  };
713816
713860
 
713817
713861
  ;// CONCATENATED MODULE: ./package.json
713818
- const package_namespaceObject = JSON.parse('{"i8":"2.11.0-snapshot-patch-claims"}');
713862
+ const package_namespaceObject = JSON.parse('{"i8":"2.11.0-snapshot-patch-claims-2"}');
713819
713863
  ;// CONCATENATED MODULE: ../../package.json
713820
713864
  const package_namespaceObject_1 = {"i8":"2.10.0"};
713821
713865
  // EXTERNAL MODULE: ../../node_modules/semver/index.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firestartr/cli",
3
- "version": "2.11.0-snapshot-patch-claims",
3
+ "version": "2.11.0-snapshot-patch-claims-2",
4
4
  "private": false,
5
5
  "description": "Commandline tool",
6
6
  "main": "build/main.js",