@harness-engineering/core 0.28.0 → 0.28.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  EntropyAnalyzer
3
- } from "./chunk-BTUDWWB4.mjs";
3
+ } from "./chunk-I2JUTTPH.mjs";
4
4
  import "./chunk-MUWJHO2S.mjs";
5
5
  import "./chunk-7P6ASYW6.mjs";
6
6
  export {
@@ -1450,15 +1450,15 @@ function generatePatternSuggestions(report) {
1450
1450
  return suggestions;
1451
1451
  }
1452
1452
  function generateSuggestions(deadCode, drift, patterns) {
1453
- const suggestions = [];
1453
+ let suggestions = [];
1454
1454
  if (deadCode) {
1455
- suggestions.push(...generateDeadCodeSuggestions(deadCode));
1455
+ suggestions = suggestions.concat(generateDeadCodeSuggestions(deadCode));
1456
1456
  }
1457
1457
  if (drift) {
1458
- suggestions.push(...generateDriftSuggestions(drift));
1458
+ suggestions = suggestions.concat(generateDriftSuggestions(drift));
1459
1459
  }
1460
1460
  if (patterns) {
1461
- suggestions.push(...generatePatternSuggestions(patterns));
1461
+ suggestions = suggestions.concat(generatePatternSuggestions(patterns));
1462
1462
  }
1463
1463
  const priorityOrder = { high: 0, medium: 1, low: 2 };
1464
1464
  suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
package/dist/index.d.mts CHANGED
@@ -5874,7 +5874,14 @@ declare class SecurityScanner {
5874
5874
  private buildSuppressionFinding;
5875
5875
  /** Check one line against a rule's patterns; return a finding or null. */
5876
5876
  private matchRuleLine;
5877
- /** Scan a single line against a resolved rule; push any findings into the array. */
5877
+ /**
5878
+ * Scan a single line against a resolved rule; push any findings into the array.
5879
+ *
5880
+ * Suppression check is two-pass: same line first (trailing-comment style), then the previous
5881
+ * line (the dominant convention: `// harness-ignore` on the line above the flagged code).
5882
+ * Without the prior-line check, every multi-line statement annotated using the over-the-top
5883
+ * convention would slip through and re-trigger the finding.
5884
+ */
5878
5885
  private scanLineForRule;
5879
5886
  /**
5880
5887
  * Core scanning loop shared by scanContent and scanContentForFile.
package/dist/index.d.ts CHANGED
@@ -5874,7 +5874,14 @@ declare class SecurityScanner {
5874
5874
  private buildSuppressionFinding;
5875
5875
  /** Check one line against a rule's patterns; return a finding or null. */
5876
5876
  private matchRuleLine;
5877
- /** Scan a single line against a resolved rule; push any findings into the array. */
5877
+ /**
5878
+ * Scan a single line against a resolved rule; push any findings into the array.
5879
+ *
5880
+ * Suppression check is two-pass: same line first (trailing-comment style), then the previous
5881
+ * line (the dominant convention: `// harness-ignore` on the line above the flagged code).
5882
+ * Without the prior-line check, every multi-line statement annotated using the over-the-top
5883
+ * convention would slip through and re-trigger the finding.
5884
+ */
5878
5885
  private scanLineForRule;
5879
5886
  /**
5880
5887
  * Core scanning loop shared by scanContent and scanContentForFile.
package/dist/index.js CHANGED
@@ -3249,15 +3249,15 @@ function generatePatternSuggestions(report) {
3249
3249
  return suggestions;
3250
3250
  }
3251
3251
  function generateSuggestions(deadCode, drift, patterns) {
3252
- const suggestions = [];
3252
+ let suggestions = [];
3253
3253
  if (deadCode) {
3254
- suggestions.push(...generateDeadCodeSuggestions(deadCode));
3254
+ suggestions = suggestions.concat(generateDeadCodeSuggestions(deadCode));
3255
3255
  }
3256
3256
  if (drift) {
3257
- suggestions.push(...generateDriftSuggestions(drift));
3257
+ suggestions = suggestions.concat(generateDriftSuggestions(drift));
3258
3258
  }
3259
3259
  if (patterns) {
3260
- suggestions.push(...generatePatternSuggestions(patterns));
3260
+ suggestions = suggestions.concat(generatePatternSuggestions(patterns));
3261
3261
  }
3262
3262
  const priorityOrder = { high: 0, medium: 1, low: 2 };
3263
3263
  suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
@@ -6102,25 +6102,8 @@ function validateBranchName(branchName, config) {
6102
6102
  }
6103
6103
  if (config.enforceKebabCase) {
6104
6104
  for (const part of slug.split("/")) {
6105
- const ticketMatch = part.match(TICKET_ID);
6106
- if (ticketMatch) {
6107
- const rest = ticketMatch[2];
6108
- if (rest && !KEBAB_CASE.test(rest)) {
6109
- return {
6110
- valid: false,
6111
- branchName,
6112
- message: `Branch slug part "${part}" does not follow kebab-case after the ticket ID.`,
6113
- suggestion: `Ensure the description after "${ticketMatch[1]}" uses kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`
6114
- };
6115
- }
6116
- } else if (!KEBAB_CASE.test(part)) {
6117
- return {
6118
- valid: false,
6119
- branchName,
6120
- message: `Branch slug part "${part}" must be in kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`,
6121
- suggestion: `Change "${part}" to match the convention.`
6122
- };
6123
- }
6105
+ const kebabFailure = validateKebabSlugPart(part, branchName);
6106
+ if (kebabFailure) return kebabFailure;
6124
6107
  }
6125
6108
  }
6126
6109
  if (typeof config.maxLength === "number" && config.maxLength > 0 && slug.length > config.maxLength) {
@@ -6133,6 +6116,26 @@ function validateBranchName(branchName, config) {
6133
6116
  }
6134
6117
  return { valid: true, branchName };
6135
6118
  }
6119
+ function validateKebabSlugPart(part, branchName) {
6120
+ const ticketMatch = part.match(TICKET_ID);
6121
+ if (ticketMatch) {
6122
+ const rest = ticketMatch[2];
6123
+ if (!rest || KEBAB_CASE.test(rest)) return null;
6124
+ return {
6125
+ valid: false,
6126
+ branchName,
6127
+ message: `Branch slug part "${part}" does not follow kebab-case after the ticket ID.`,
6128
+ suggestion: `Ensure the description after "${ticketMatch[1]}" uses kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`
6129
+ };
6130
+ }
6131
+ if (KEBAB_CASE.test(part)) return null;
6132
+ return {
6133
+ valid: false,
6134
+ branchName,
6135
+ message: `Branch slug part "${part}" must be in kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`,
6136
+ suggestion: `Change "${part}" to match the convention.`
6137
+ };
6138
+ }
6136
6139
 
6137
6140
  // src/context/doc-coverage.ts
6138
6141
  var import_minimatch2 = require("minimatch");
@@ -8125,15 +8128,21 @@ async function gatherDecayBlock(projectPath) {
8125
8128
  const { TimelineManager: TimelineManager2 } = await Promise.resolve().then(() => (init_timeline_manager(), timeline_manager_exports));
8126
8129
  const mgr = new TimelineManager2(projectPath);
8127
8130
  const trends = mgr.trends();
8128
- const recentBumps = arrayLen(trends.recentSnapshots);
8129
- const affected = Array.isArray(trends.topAffected) ? trends.topAffected : [];
8130
- const topAffected = [];
8131
+ return {
8132
+ recentBumps: arrayLen(trends.recentSnapshots),
8133
+ topAffected: collectTopAffectedLabels(trends.topAffected)
8134
+ };
8135
+ }
8136
+ var MAX_TOP_AFFECTED = 5;
8137
+ function collectTopAffectedLabels(affected) {
8138
+ if (!Array.isArray(affected)) return [];
8139
+ const out = [];
8131
8140
  for (const node of affected) {
8132
8141
  const label = node.id ?? node.name;
8133
- if (typeof label === "string" && label.length > 0) topAffected.push(label);
8134
- if (topAffected.length >= 5) break;
8142
+ if (typeof label === "string" && label.length > 0) out.push(label);
8143
+ if (out.length >= MAX_TOP_AFFECTED) break;
8135
8144
  }
8136
- return { recentBumps, topAffected };
8145
+ return out;
8137
8146
  }
8138
8147
  async function gatherAttentionBlock(projectPath) {
8139
8148
  const sessionsDir = path4.join(projectPath, ".harness", "sessions");
@@ -14639,11 +14648,20 @@ var SecurityScanner = class {
14639
14648
  }
14640
14649
  return null;
14641
14650
  }
14642
- /** Scan a single line against a resolved rule; push any findings into the array. */
14643
- scanLineForRule(rule, resolved, line, lineNumber, filePath, findings) {
14644
- const suppressionMatch = parseHarnessIgnore(line, rule.id);
14651
+ /**
14652
+ * Scan a single line against a resolved rule; push any findings into the array.
14653
+ *
14654
+ * Suppression check is two-pass: same line first (trailing-comment style), then the previous
14655
+ * line (the dominant convention: `// harness-ignore` on the line above the flagged code).
14656
+ * Without the prior-line check, every multi-line statement annotated using the over-the-top
14657
+ * convention would slip through and re-trigger the finding.
14658
+ */
14659
+ scanLineForRule(rule, resolved, line, lineNumber, filePath, findings, priorLine) {
14660
+ const sameLineMatch = parseHarnessIgnore(line, rule.id);
14661
+ const priorLineMatch = !sameLineMatch && priorLine !== void 0 ? parseHarnessIgnore(priorLine, rule.id) : null;
14662
+ const suppressionMatch = sameLineMatch ?? priorLineMatch;
14645
14663
  if (suppressionMatch) {
14646
- if (!suppressionMatch.justification) {
14664
+ if (!suppressionMatch.justification && sameLineMatch !== null) {
14647
14665
  findings.push(this.buildSuppressionFinding(rule, filePath, lineNumber, line));
14648
14666
  }
14649
14667
  return;
@@ -14667,7 +14685,15 @@ var SecurityScanner = class {
14667
14685
  );
14668
14686
  if (resolved === "off") continue;
14669
14687
  for (let i = 0; i < lines.length; i++) {
14670
- this.scanLineForRule(rule, resolved, lines[i] ?? "", startLine + i, filePath, findings);
14688
+ this.scanLineForRule(
14689
+ rule,
14690
+ resolved,
14691
+ lines[i] ?? "",
14692
+ startLine + i,
14693
+ filePath,
14694
+ findings,
14695
+ i > 0 ? lines[i - 1] : void 0
14696
+ );
14671
14697
  }
14672
14698
  }
14673
14699
  return findings;
@@ -19782,13 +19808,20 @@ function arraysEqual2(a, b) {
19782
19808
  // src/roadmap/tracker/adapters/github-issues.ts
19783
19809
  function metaFromFeatureFields(src) {
19784
19810
  const meta = {};
19785
- if (src.spec !== void 0 && src.spec !== null) meta.spec = src.spec;
19786
- if (src.plans && src.plans.length > 0) meta.plan = src.plans[0];
19787
- if (src.blockedBy && src.blockedBy.length > 0) meta.blocked_by = src.blockedBy;
19788
- if (src.priority !== void 0 && src.priority !== null) meta.priority = src.priority;
19789
- if (src.milestone !== void 0 && src.milestone !== null) meta.milestone = src.milestone;
19811
+ assignNonNullish(meta, "spec", src.spec);
19812
+ if (hasItems(src.plans)) meta.plan = src.plans[0];
19813
+ if (hasItems(src.blockedBy)) meta.blocked_by = src.blockedBy;
19814
+ assignNonNullish(meta, "priority", src.priority);
19815
+ assignNonNullish(meta, "milestone", src.milestone);
19790
19816
  return meta;
19791
19817
  }
19818
+ function assignNonNullish(meta, key, value) {
19819
+ if (value === void 0 || value === null) return;
19820
+ meta[key] = value;
19821
+ }
19822
+ function hasItems(arr) {
19823
+ return Array.isArray(arr) && arr.length > 0;
19824
+ }
19792
19825
  function buildCreateMeta(feature) {
19793
19826
  return metaFromFeatureFields(feature);
19794
19827
  }
@@ -20449,33 +20482,42 @@ function featureToNewInput(feature, milestone) {
20449
20482
  return input;
20450
20483
  }
20451
20484
  function metaToPatch(feature, expected) {
20452
- const patch = {};
20453
- patch.summary = feature.summary;
20454
- if (expected.spec !== void 0) patch.spec = expected.spec ?? null;
20455
- if (expected.plan !== void 0 && expected.plan != null) patch.plans = [expected.plan];
20456
- if (expected.blocked_by !== void 0) patch.blockedBy = expected.blocked_by ?? [];
20457
- if (expected.priority !== void 0) patch.priority = expected.priority ?? null;
20458
- if (expected.milestone !== void 0) patch.milestone = expected.milestone ?? null;
20485
+ const patch = { summary: feature.summary };
20486
+ assignIfPresent(patch, "spec", expected.spec, null);
20487
+ if (expected.plan != null) patch.plans = [expected.plan];
20488
+ assignIfPresent(patch, "blockedBy", expected.blocked_by, []);
20489
+ assignIfPresent(patch, "priority", expected.priority, null);
20490
+ assignIfPresent(patch, "milestone", expected.milestone, null);
20459
20491
  return patch;
20460
20492
  }
20493
+ function assignIfPresent(patch, key, value, fallback2) {
20494
+ if (value === void 0) return;
20495
+ patch[key] = value ?? fallback2;
20496
+ }
20461
20497
  function formatDiff(actual, expected) {
20462
- const keys = /* @__PURE__ */ new Set();
20463
- const collect = (m) => {
20464
- for (const k of Object.keys(m)) {
20465
- const v = m[k];
20466
- if (v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0)) keys.add(k);
20467
- }
20468
- };
20469
- collect(actual);
20470
- collect(expected);
20498
+ const keys = collectPresentKeys(actual, expected);
20471
20499
  const changed = [];
20472
- for (const key of [...keys].sort()) {
20500
+ for (const key of keys) {
20473
20501
  const a = actual[key];
20474
20502
  const e = expected[key];
20475
20503
  if (JSON.stringify(a ?? null) !== JSON.stringify(e ?? null)) changed.push(key);
20476
20504
  }
20477
20505
  return changed.join(",");
20478
20506
  }
20507
+ function hasMeaningfulValue(value) {
20508
+ if (value === void 0 || value === null) return false;
20509
+ if (Array.isArray(value) && value.length === 0) return false;
20510
+ return true;
20511
+ }
20512
+ function collectPresentKeys(...metas) {
20513
+ const keys = /* @__PURE__ */ new Set();
20514
+ for (const m of metas) {
20515
+ for (const [k, v] of Object.entries(m)) {
20516
+ if (hasMeaningfulValue(v)) keys.add(k);
20517
+ }
20518
+ }
20519
+ return [...keys].sort();
20520
+ }
20479
20521
  function mapAction(action) {
20480
20522
  switch (action) {
20481
20523
  case "assigned":
@@ -22166,20 +22208,43 @@ var RETRY_BACKOFFS_MS = [1e3, 2e3, 4e3];
22166
22208
  function toUnixNanoString(ns) {
22167
22209
  return ns.toString(10);
22168
22210
  }
22211
+ function encodeAttributeValue(value) {
22212
+ if (typeof value === "string") return { stringValue: value };
22213
+ if (typeof value === "boolean") return { boolValue: value };
22214
+ if (typeof value === "number") {
22215
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
22216
+ }
22217
+ return null;
22218
+ }
22219
+ var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
22220
+ var DEFAULT_BATCH_SIZE = 64;
22221
+ var defaultFetch = (...args) => globalThis.fetch(...args);
22222
+ var defaultWarn = (...args) => console.warn(...args);
22223
+ function resolveOTLPOptions(opts) {
22224
+ const {
22225
+ endpoint,
22226
+ enabled = true,
22227
+ headers = {},
22228
+ flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
22229
+ batchSize = DEFAULT_BATCH_SIZE,
22230
+ fetchImpl = defaultFetch,
22231
+ warn = defaultWarn
22232
+ } = opts;
22233
+ return {
22234
+ endpoint,
22235
+ enabled,
22236
+ headers: { "Content-Type": "application/json", ...headers },
22237
+ flushIntervalMs,
22238
+ batchSize,
22239
+ fetchImpl,
22240
+ warn
22241
+ };
22242
+ }
22169
22243
  function attributesToOTLP(attrs) {
22170
22244
  const out = [];
22171
22245
  for (const [key, value] of Object.entries(attrs)) {
22172
- if (typeof value === "string") {
22173
- out.push({ key, value: { stringValue: value } });
22174
- } else if (typeof value === "boolean") {
22175
- out.push({ key, value: { boolValue: value } });
22176
- } else if (typeof value === "number") {
22177
- if (Number.isInteger(value)) {
22178
- out.push({ key, value: { intValue: String(value) } });
22179
- } else {
22180
- out.push({ key, value: { doubleValue: value } });
22181
- }
22182
- }
22246
+ const encoded = encodeAttributeValue(value);
22247
+ if (encoded) out.push({ key, value: encoded });
22183
22248
  }
22184
22249
  return out;
22185
22250
  }
@@ -22195,13 +22260,14 @@ var OTLPExporter = class {
22195
22260
  timer = null;
22196
22261
  inFlightFlushes = /* @__PURE__ */ new Set();
22197
22262
  constructor(opts) {
22198
- this.endpoint = opts.endpoint;
22199
- this.enabled = opts.enabled !== false;
22200
- this.headers = { "Content-Type": "application/json", ...opts.headers ?? {} };
22201
- this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
22202
- this.batchSize = opts.batchSize ?? 64;
22203
- this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
22204
- this.warn = opts.warn ?? ((...a) => console.warn(...a));
22263
+ const resolved = resolveOTLPOptions(opts);
22264
+ this.endpoint = resolved.endpoint;
22265
+ this.enabled = resolved.enabled;
22266
+ this.headers = resolved.headers;
22267
+ this.flushIntervalMs = resolved.flushIntervalMs;
22268
+ this.batchSize = resolved.batchSize;
22269
+ this.fetchImpl = resolved.fetchImpl;
22270
+ this.warn = resolved.warn;
22205
22271
  }
22206
22272
  /**
22207
22273
  * O(1) buffer push. When `enabled === false` this is a no-op. If the
@@ -22284,36 +22350,32 @@ var OTLPExporter = class {
22284
22350
  * tests; not part of the supported API surface.
22285
22351
  */
22286
22352
  spansToOTLPJSON(spans) {
22287
- return {
22288
- resourceSpans: [
22289
- {
22290
- resource: {
22291
- attributes: [{ key: "service.name", value: { stringValue: "harness" } }]
22292
- },
22293
- scopeSpans: [
22294
- {
22295
- scope: { name: "harness" },
22296
- spans: spans.map((s) => {
22297
- const span = {
22298
- traceId: s.traceId,
22299
- spanId: s.spanId,
22300
- name: s.name,
22301
- kind: s.kind,
22302
- startTimeUnixNano: toUnixNanoString(s.startTimeNs),
22303
- endTimeUnixNano: toUnixNanoString(s.endTimeNs),
22304
- attributes: attributesToOTLP(s.attributes)
22305
- };
22306
- if (s.parentSpanId !== void 0) span["parentSpanId"] = s.parentSpanId;
22307
- if (s.statusCode !== void 0) span["status"] = { code: s.statusCode };
22308
- return span;
22309
- })
22310
- }
22311
- ]
22312
- }
22313
- ]
22353
+ const scopeSpan = {
22354
+ scope: { name: "harness" },
22355
+ spans: spans.map(spanToOTLP)
22356
+ };
22357
+ const resourceSpan = {
22358
+ resource: { attributes: SERVICE_NAME_ATTR },
22359
+ scopeSpans: [scopeSpan]
22314
22360
  };
22361
+ return { resourceSpans: [resourceSpan] };
22315
22362
  }
22316
22363
  };
22364
+ var SERVICE_NAME_ATTR = [{ key: "service.name", value: { stringValue: "harness" } }];
22365
+ function spanToOTLP(s) {
22366
+ const span = {
22367
+ traceId: s.traceId,
22368
+ spanId: s.spanId,
22369
+ name: s.name,
22370
+ kind: s.kind,
22371
+ startTimeUnixNano: toUnixNanoString(s.startTimeNs),
22372
+ endTimeUnixNano: toUnixNanoString(s.endTimeNs),
22373
+ attributes: attributesToOTLP(s.attributes)
22374
+ };
22375
+ if (s.parentSpanId !== void 0) span["parentSpanId"] = s.parentSpanId;
22376
+ if (s.statusCode !== void 0) span["status"] = { code: s.statusCode };
22377
+ return span;
22378
+ }
22317
22379
 
22318
22380
  // src/telemetry/exporter/types.ts
22319
22381
  var SpanKind = /* @__PURE__ */ ((SpanKind2) => {
@@ -22429,15 +22491,22 @@ var PII_TOKENS = [
22429
22491
  var PII_FIELD_DENYLIST = new RegExp(`^(?:${PII_TOKENS.join("|")})$`, "i");
22430
22492
  var PII_LINE_RE = new RegExp(`\\b(?:${PII_TOKENS.join("|")})\\b`, "i");
22431
22493
  var ALLOWED_SET = new Set(ALLOWED_FIELD_KEYS);
22432
- function isSanitizedResult(value) {
22433
- if (!value || typeof value !== "object") return false;
22434
- const v = value;
22435
- if (!v.fields || typeof v.fields !== "object") return false;
22436
- for (const k of Object.keys(v.fields)) {
22494
+ function isObject(value) {
22495
+ return Boolean(value) && typeof value === "object";
22496
+ }
22497
+ function hasAllowedFieldKeys(fields) {
22498
+ for (const k of Object.keys(fields)) {
22437
22499
  if (!ALLOWED_SET.has(k)) return false;
22438
22500
  if (PII_FIELD_DENYLIST.test(k)) return false;
22439
22501
  }
22440
- if (!v.distributions || typeof v.distributions !== "object") return false;
22502
+ return true;
22503
+ }
22504
+ function isSanitizedResult(value) {
22505
+ if (!isObject(value)) return false;
22506
+ const { fields, distributions } = value;
22507
+ if (!isObject(fields)) return false;
22508
+ if (!hasAllowedFieldKeys(fields)) return false;
22509
+ if (!isObject(distributions)) return false;
22441
22510
  return true;
22442
22511
  }
22443
22512
  function assertSanitized(value) {
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  detectSizeBudgetViolations,
28
28
  generateSuggestions,
29
29
  parseSize
30
- } from "./chunk-BTUDWWB4.mjs";
30
+ } from "./chunk-I2JUTTPH.mjs";
31
31
  import {
32
32
  EXTENSION_MAP,
33
33
  Err,
@@ -1779,25 +1779,8 @@ function validateBranchName(branchName, config) {
1779
1779
  }
1780
1780
  if (config.enforceKebabCase) {
1781
1781
  for (const part of slug.split("/")) {
1782
- const ticketMatch = part.match(TICKET_ID);
1783
- if (ticketMatch) {
1784
- const rest = ticketMatch[2];
1785
- if (rest && !KEBAB_CASE.test(rest)) {
1786
- return {
1787
- valid: false,
1788
- branchName,
1789
- message: `Branch slug part "${part}" does not follow kebab-case after the ticket ID.`,
1790
- suggestion: `Ensure the description after "${ticketMatch[1]}" uses kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`
1791
- };
1792
- }
1793
- } else if (!KEBAB_CASE.test(part)) {
1794
- return {
1795
- valid: false,
1796
- branchName,
1797
- message: `Branch slug part "${part}" must be in kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`,
1798
- suggestion: `Change "${part}" to match the convention.`
1799
- };
1800
- }
1782
+ const kebabFailure = validateKebabSlugPart(part, branchName);
1783
+ if (kebabFailure) return kebabFailure;
1801
1784
  }
1802
1785
  }
1803
1786
  if (typeof config.maxLength === "number" && config.maxLength > 0 && slug.length > config.maxLength) {
@@ -1810,6 +1793,26 @@ function validateBranchName(branchName, config) {
1810
1793
  }
1811
1794
  return { valid: true, branchName };
1812
1795
  }
1796
+ function validateKebabSlugPart(part, branchName) {
1797
+ const ticketMatch = part.match(TICKET_ID);
1798
+ if (ticketMatch) {
1799
+ const rest = ticketMatch[2];
1800
+ if (!rest || KEBAB_CASE.test(rest)) return null;
1801
+ return {
1802
+ valid: false,
1803
+ branchName,
1804
+ message: `Branch slug part "${part}" does not follow kebab-case after the ticket ID.`,
1805
+ suggestion: `Ensure the description after "${ticketMatch[1]}" uses kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`
1806
+ };
1807
+ }
1808
+ if (KEBAB_CASE.test(part)) return null;
1809
+ return {
1810
+ valid: false,
1811
+ branchName,
1812
+ message: `Branch slug part "${part}" must be in kebab-case (lowercase, single hyphens, no leading/trailing hyphen).`,
1813
+ suggestion: `Change "${part}" to match the convention.`
1814
+ };
1815
+ }
1813
1816
 
1814
1817
  // src/context/doc-coverage.ts
1815
1818
  import { minimatch as minimatch2 } from "minimatch";
@@ -3650,7 +3653,7 @@ function arrayLen(arr) {
3650
3653
  return Array.isArray(arr) ? arr.length : 0;
3651
3654
  }
3652
3655
  async function gatherEntropyBlock(projectPath) {
3653
- const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./analyzer-VY6DJVOU.mjs");
3656
+ const { EntropyAnalyzer: EntropyAnalyzer2 } = await import("./analyzer-H3AHBFSL.mjs");
3654
3657
  const analyzer = new EntropyAnalyzer2({
3655
3658
  rootDir: projectPath,
3656
3659
  include: ["src/**/*.ts", "src/**/*.tsx", "packages/*/src/**/*.ts"],
@@ -3671,15 +3674,21 @@ async function gatherDecayBlock(projectPath) {
3671
3674
  const { TimelineManager: TimelineManager2 } = await import("./timeline-manager-FPYKJRHR.mjs");
3672
3675
  const mgr = new TimelineManager2(projectPath);
3673
3676
  const trends = mgr.trends();
3674
- const recentBumps = arrayLen(trends.recentSnapshots);
3675
- const affected = Array.isArray(trends.topAffected) ? trends.topAffected : [];
3676
- const topAffected = [];
3677
+ return {
3678
+ recentBumps: arrayLen(trends.recentSnapshots),
3679
+ topAffected: collectTopAffectedLabels(trends.topAffected)
3680
+ };
3681
+ }
3682
+ var MAX_TOP_AFFECTED = 5;
3683
+ function collectTopAffectedLabels(affected) {
3684
+ if (!Array.isArray(affected)) return [];
3685
+ const out = [];
3677
3686
  for (const node of affected) {
3678
3687
  const label = node.id ?? node.name;
3679
- if (typeof label === "string" && label.length > 0) topAffected.push(label);
3680
- if (topAffected.length >= 5) break;
3688
+ if (typeof label === "string" && label.length > 0) out.push(label);
3689
+ if (out.length >= MAX_TOP_AFFECTED) break;
3681
3690
  }
3682
- return { recentBumps, topAffected };
3691
+ return out;
3683
3692
  }
3684
3693
  async function gatherAttentionBlock(projectPath) {
3685
3694
  const sessionsDir = path4.join(projectPath, ".harness", "sessions");
@@ -9090,11 +9099,20 @@ var SecurityScanner = class {
9090
9099
  }
9091
9100
  return null;
9092
9101
  }
9093
- /** Scan a single line against a resolved rule; push any findings into the array. */
9094
- scanLineForRule(rule, resolved, line, lineNumber, filePath, findings) {
9095
- const suppressionMatch = parseHarnessIgnore(line, rule.id);
9102
+ /**
9103
+ * Scan a single line against a resolved rule; push any findings into the array.
9104
+ *
9105
+ * Suppression check is two-pass: same line first (trailing-comment style), then the previous
9106
+ * line (the dominant convention: `// harness-ignore` on the line above the flagged code).
9107
+ * Without the prior-line check, every multi-line statement annotated using the over-the-top
9108
+ * convention would slip through and re-trigger the finding.
9109
+ */
9110
+ scanLineForRule(rule, resolved, line, lineNumber, filePath, findings, priorLine) {
9111
+ const sameLineMatch = parseHarnessIgnore(line, rule.id);
9112
+ const priorLineMatch = !sameLineMatch && priorLine !== void 0 ? parseHarnessIgnore(priorLine, rule.id) : null;
9113
+ const suppressionMatch = sameLineMatch ?? priorLineMatch;
9096
9114
  if (suppressionMatch) {
9097
- if (!suppressionMatch.justification) {
9115
+ if (!suppressionMatch.justification && sameLineMatch !== null) {
9098
9116
  findings.push(this.buildSuppressionFinding(rule, filePath, lineNumber, line));
9099
9117
  }
9100
9118
  return;
@@ -9118,7 +9136,15 @@ var SecurityScanner = class {
9118
9136
  );
9119
9137
  if (resolved === "off") continue;
9120
9138
  for (let i = 0; i < lines.length; i++) {
9121
- this.scanLineForRule(rule, resolved, lines[i] ?? "", startLine + i, filePath, findings);
9139
+ this.scanLineForRule(
9140
+ rule,
9141
+ resolved,
9142
+ lines[i] ?? "",
9143
+ startLine + i,
9144
+ filePath,
9145
+ findings,
9146
+ i > 0 ? lines[i - 1] : void 0
9147
+ );
9122
9148
  }
9123
9149
  }
9124
9150
  return findings;
@@ -14224,13 +14250,20 @@ function arraysEqual2(a, b) {
14224
14250
  // src/roadmap/tracker/adapters/github-issues.ts
14225
14251
  function metaFromFeatureFields(src) {
14226
14252
  const meta = {};
14227
- if (src.spec !== void 0 && src.spec !== null) meta.spec = src.spec;
14228
- if (src.plans && src.plans.length > 0) meta.plan = src.plans[0];
14229
- if (src.blockedBy && src.blockedBy.length > 0) meta.blocked_by = src.blockedBy;
14230
- if (src.priority !== void 0 && src.priority !== null) meta.priority = src.priority;
14231
- if (src.milestone !== void 0 && src.milestone !== null) meta.milestone = src.milestone;
14253
+ assignNonNullish(meta, "spec", src.spec);
14254
+ if (hasItems(src.plans)) meta.plan = src.plans[0];
14255
+ if (hasItems(src.blockedBy)) meta.blocked_by = src.blockedBy;
14256
+ assignNonNullish(meta, "priority", src.priority);
14257
+ assignNonNullish(meta, "milestone", src.milestone);
14232
14258
  return meta;
14233
14259
  }
14260
+ function assignNonNullish(meta, key, value) {
14261
+ if (value === void 0 || value === null) return;
14262
+ meta[key] = value;
14263
+ }
14264
+ function hasItems(arr) {
14265
+ return Array.isArray(arr) && arr.length > 0;
14266
+ }
14234
14267
  function buildCreateMeta(feature) {
14235
14268
  return metaFromFeatureFields(feature);
14236
14269
  }
@@ -14891,33 +14924,42 @@ function featureToNewInput(feature, milestone) {
14891
14924
  return input;
14892
14925
  }
14893
14926
  function metaToPatch(feature, expected) {
14894
- const patch = {};
14895
- patch.summary = feature.summary;
14896
- if (expected.spec !== void 0) patch.spec = expected.spec ?? null;
14897
- if (expected.plan !== void 0 && expected.plan != null) patch.plans = [expected.plan];
14898
- if (expected.blocked_by !== void 0) patch.blockedBy = expected.blocked_by ?? [];
14899
- if (expected.priority !== void 0) patch.priority = expected.priority ?? null;
14900
- if (expected.milestone !== void 0) patch.milestone = expected.milestone ?? null;
14927
+ const patch = { summary: feature.summary };
14928
+ assignIfPresent(patch, "spec", expected.spec, null);
14929
+ if (expected.plan != null) patch.plans = [expected.plan];
14930
+ assignIfPresent(patch, "blockedBy", expected.blocked_by, []);
14931
+ assignIfPresent(patch, "priority", expected.priority, null);
14932
+ assignIfPresent(patch, "milestone", expected.milestone, null);
14901
14933
  return patch;
14902
14934
  }
14935
+ function assignIfPresent(patch, key, value, fallback2) {
14936
+ if (value === void 0) return;
14937
+ patch[key] = value ?? fallback2;
14938
+ }
14903
14939
  function formatDiff(actual, expected) {
14904
- const keys = /* @__PURE__ */ new Set();
14905
- const collect = (m) => {
14906
- for (const k of Object.keys(m)) {
14907
- const v = m[k];
14908
- if (v !== void 0 && v !== null && !(Array.isArray(v) && v.length === 0)) keys.add(k);
14909
- }
14910
- };
14911
- collect(actual);
14912
- collect(expected);
14940
+ const keys = collectPresentKeys(actual, expected);
14913
14941
  const changed = [];
14914
- for (const key of [...keys].sort()) {
14942
+ for (const key of keys) {
14915
14943
  const a = actual[key];
14916
14944
  const e = expected[key];
14917
14945
  if (JSON.stringify(a ?? null) !== JSON.stringify(e ?? null)) changed.push(key);
14918
14946
  }
14919
14947
  return changed.join(",");
14920
14948
  }
14949
+ function hasMeaningfulValue(value) {
14950
+ if (value === void 0 || value === null) return false;
14951
+ if (Array.isArray(value) && value.length === 0) return false;
14952
+ return true;
14953
+ }
14954
+ function collectPresentKeys(...metas) {
14955
+ const keys = /* @__PURE__ */ new Set();
14956
+ for (const m of metas) {
14957
+ for (const [k, v] of Object.entries(m)) {
14958
+ if (hasMeaningfulValue(v)) keys.add(k);
14959
+ }
14960
+ }
14961
+ return [...keys].sort();
14962
+ }
14921
14963
  function mapAction(action) {
14922
14964
  switch (action) {
14923
14965
  case "assigned":
@@ -16595,20 +16637,43 @@ var RETRY_BACKOFFS_MS = [1e3, 2e3, 4e3];
16595
16637
  function toUnixNanoString(ns) {
16596
16638
  return ns.toString(10);
16597
16639
  }
16640
+ function encodeAttributeValue(value) {
16641
+ if (typeof value === "string") return { stringValue: value };
16642
+ if (typeof value === "boolean") return { boolValue: value };
16643
+ if (typeof value === "number") {
16644
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
16645
+ }
16646
+ return null;
16647
+ }
16648
+ var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
16649
+ var DEFAULT_BATCH_SIZE = 64;
16650
+ var defaultFetch = (...args) => globalThis.fetch(...args);
16651
+ var defaultWarn = (...args) => console.warn(...args);
16652
+ function resolveOTLPOptions(opts) {
16653
+ const {
16654
+ endpoint,
16655
+ enabled = true,
16656
+ headers = {},
16657
+ flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
16658
+ batchSize = DEFAULT_BATCH_SIZE,
16659
+ fetchImpl = defaultFetch,
16660
+ warn = defaultWarn
16661
+ } = opts;
16662
+ return {
16663
+ endpoint,
16664
+ enabled,
16665
+ headers: { "Content-Type": "application/json", ...headers },
16666
+ flushIntervalMs,
16667
+ batchSize,
16668
+ fetchImpl,
16669
+ warn
16670
+ };
16671
+ }
16598
16672
  function attributesToOTLP(attrs) {
16599
16673
  const out = [];
16600
16674
  for (const [key, value] of Object.entries(attrs)) {
16601
- if (typeof value === "string") {
16602
- out.push({ key, value: { stringValue: value } });
16603
- } else if (typeof value === "boolean") {
16604
- out.push({ key, value: { boolValue: value } });
16605
- } else if (typeof value === "number") {
16606
- if (Number.isInteger(value)) {
16607
- out.push({ key, value: { intValue: String(value) } });
16608
- } else {
16609
- out.push({ key, value: { doubleValue: value } });
16610
- }
16611
- }
16675
+ const encoded = encodeAttributeValue(value);
16676
+ if (encoded) out.push({ key, value: encoded });
16612
16677
  }
16613
16678
  return out;
16614
16679
  }
@@ -16624,13 +16689,14 @@ var OTLPExporter = class {
16624
16689
  timer = null;
16625
16690
  inFlightFlushes = /* @__PURE__ */ new Set();
16626
16691
  constructor(opts) {
16627
- this.endpoint = opts.endpoint;
16628
- this.enabled = opts.enabled !== false;
16629
- this.headers = { "Content-Type": "application/json", ...opts.headers ?? {} };
16630
- this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
16631
- this.batchSize = opts.batchSize ?? 64;
16632
- this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
16633
- this.warn = opts.warn ?? ((...a) => console.warn(...a));
16692
+ const resolved = resolveOTLPOptions(opts);
16693
+ this.endpoint = resolved.endpoint;
16694
+ this.enabled = resolved.enabled;
16695
+ this.headers = resolved.headers;
16696
+ this.flushIntervalMs = resolved.flushIntervalMs;
16697
+ this.batchSize = resolved.batchSize;
16698
+ this.fetchImpl = resolved.fetchImpl;
16699
+ this.warn = resolved.warn;
16634
16700
  }
16635
16701
  /**
16636
16702
  * O(1) buffer push. When `enabled === false` this is a no-op. If the
@@ -16713,36 +16779,32 @@ var OTLPExporter = class {
16713
16779
  * tests; not part of the supported API surface.
16714
16780
  */
16715
16781
  spansToOTLPJSON(spans) {
16716
- return {
16717
- resourceSpans: [
16718
- {
16719
- resource: {
16720
- attributes: [{ key: "service.name", value: { stringValue: "harness" } }]
16721
- },
16722
- scopeSpans: [
16723
- {
16724
- scope: { name: "harness" },
16725
- spans: spans.map((s) => {
16726
- const span = {
16727
- traceId: s.traceId,
16728
- spanId: s.spanId,
16729
- name: s.name,
16730
- kind: s.kind,
16731
- startTimeUnixNano: toUnixNanoString(s.startTimeNs),
16732
- endTimeUnixNano: toUnixNanoString(s.endTimeNs),
16733
- attributes: attributesToOTLP(s.attributes)
16734
- };
16735
- if (s.parentSpanId !== void 0) span["parentSpanId"] = s.parentSpanId;
16736
- if (s.statusCode !== void 0) span["status"] = { code: s.statusCode };
16737
- return span;
16738
- })
16739
- }
16740
- ]
16741
- }
16742
- ]
16782
+ const scopeSpan = {
16783
+ scope: { name: "harness" },
16784
+ spans: spans.map(spanToOTLP)
16785
+ };
16786
+ const resourceSpan = {
16787
+ resource: { attributes: SERVICE_NAME_ATTR },
16788
+ scopeSpans: [scopeSpan]
16743
16789
  };
16790
+ return { resourceSpans: [resourceSpan] };
16744
16791
  }
16745
16792
  };
16793
+ var SERVICE_NAME_ATTR = [{ key: "service.name", value: { stringValue: "harness" } }];
16794
+ function spanToOTLP(s) {
16795
+ const span = {
16796
+ traceId: s.traceId,
16797
+ spanId: s.spanId,
16798
+ name: s.name,
16799
+ kind: s.kind,
16800
+ startTimeUnixNano: toUnixNanoString(s.startTimeNs),
16801
+ endTimeUnixNano: toUnixNanoString(s.endTimeNs),
16802
+ attributes: attributesToOTLP(s.attributes)
16803
+ };
16804
+ if (s.parentSpanId !== void 0) span["parentSpanId"] = s.parentSpanId;
16805
+ if (s.statusCode !== void 0) span["status"] = { code: s.statusCode };
16806
+ return span;
16807
+ }
16746
16808
 
16747
16809
  // src/telemetry/exporter/types.ts
16748
16810
  var SpanKind = /* @__PURE__ */ ((SpanKind2) => {
@@ -16858,15 +16920,22 @@ var PII_TOKENS = [
16858
16920
  var PII_FIELD_DENYLIST = new RegExp(`^(?:${PII_TOKENS.join("|")})$`, "i");
16859
16921
  var PII_LINE_RE = new RegExp(`\\b(?:${PII_TOKENS.join("|")})\\b`, "i");
16860
16922
  var ALLOWED_SET = new Set(ALLOWED_FIELD_KEYS);
16861
- function isSanitizedResult(value) {
16862
- if (!value || typeof value !== "object") return false;
16863
- const v = value;
16864
- if (!v.fields || typeof v.fields !== "object") return false;
16865
- for (const k of Object.keys(v.fields)) {
16923
+ function isObject(value) {
16924
+ return Boolean(value) && typeof value === "object";
16925
+ }
16926
+ function hasAllowedFieldKeys(fields) {
16927
+ for (const k of Object.keys(fields)) {
16866
16928
  if (!ALLOWED_SET.has(k)) return false;
16867
16929
  if (PII_FIELD_DENYLIST.test(k)) return false;
16868
16930
  }
16869
- if (!v.distributions || typeof v.distributions !== "object") return false;
16931
+ return true;
16932
+ }
16933
+ function isSanitizedResult(value) {
16934
+ if (!isObject(value)) return false;
16935
+ const { fields, distributions } = value;
16936
+ if (!isObject(fields)) return false;
16937
+ if (!hasAllowedFieldKeys(fields)) return false;
16938
+ if (!isObject(distributions)) return false;
16870
16939
  return true;
16871
16940
  }
16872
16941
  function assertSanitized(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/core",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Core library for Harness Engineering toolkit",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -51,8 +51,8 @@
51
51
  "web-tree-sitter": "^0.24.7",
52
52
  "yaml": "^2.8.3",
53
53
  "zod": "^3.25.76",
54
- "@harness-engineering/graph": "0.9.0",
55
- "@harness-engineering/types": "0.14.0"
54
+ "@harness-engineering/graph": "0.10.0",
55
+ "@harness-engineering/types": "0.15.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/ejs": "^3.1.5",