@skill-map/cli 0.56.0 → 0.57.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="49fdc2af-2694-5cd0-ac19-33f8d1dffa9e")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e6038423-ea97-5e66-b371-0916d382d819")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.56.0",
253
+ version: "0.57.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -1815,16 +1815,25 @@ function applyAjvFormats(ajv) {
1815
1815
  addFormats(ajv);
1816
1816
  }
1817
1817
 
1818
+ // kernel/util/finding-format.ts
1819
+ function formatFinding(parts) {
1820
+ const head = parts.subject ? `\`${parts.subject}\`:
1821
+ ` : "";
1822
+ const loc = parts.lines && parts.lines.length > 0 ? `L${parts.lines.join(", ")}: ` : "";
1823
+ return `${head}${loc}${parts.body}`;
1824
+ }
1825
+
1818
1826
  // plugins/core/analyzers/annotation-field-unknown/text.ts
1819
1827
  var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1820
- // Compact finding grammar: the affected node is the finding's own
1821
- // node, so its path never appears in the message.
1828
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
1829
+ // owns the subject (the offending key, built before the call); the
1830
+ // affected node is the finding's own node, so its path never appears.
1822
1831
  /** Key inside `annotations:` is not in the curated catalog. */
1823
- unknownAnnotationKey: "Unknown sidecar key '{{key}}'; not in the annotations catalog.",
1832
+ unknownAnnotationKey: "Unknown sidecar key; not in the annotations catalog",
1824
1833
  /** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
1825
- unknownRootKey: "Unknown sidecar top-level key '{{key}}'; not a reserved block, a plugin namespace, or a root contribution.",
1834
+ unknownRootKey: "Unknown top-level sidecar key; not a reserved block, plugin namespace, or root contribution",
1826
1835
  /** Value under a registered plugin namespace fails the contributed schema. */
1827
- pluginNamespaceInvalid: "Sidecar block '{{pluginId}}.{{key}}' fails the schema from plugin '{{pluginId}}': {{errors}}.",
1836
+ pluginNamespaceInvalid: "Sidecar block fails the plugin schema; {{errors}}",
1828
1837
  // Tooltips for the per-node view-contribution badges. Singular vs
1829
1838
  // plural keeps the count grammar correct without a sub-template.
1830
1839
  alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
@@ -1877,9 +1886,9 @@ var annotationFieldUnknownAnalyzer = {
1877
1886
  analyzerId: ID9,
1878
1887
  severity: "warn",
1879
1888
  nodeIds: [node.path],
1880
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
1881
- path: node.path,
1882
- key
1889
+ message: formatFinding({
1890
+ subject: key,
1891
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey)
1883
1892
  }),
1884
1893
  data: { surface: "annotations", key }
1885
1894
  });
@@ -1904,11 +1913,11 @@ var annotationFieldUnknownAnalyzer = {
1904
1913
  analyzerId: ID9,
1905
1914
  severity: "warn",
1906
1915
  nodeIds: [node.path],
1907
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
1908
- path: node.path,
1909
- pluginId: key,
1910
- key: contribKey,
1911
- errors
1916
+ message: formatFinding({
1917
+ subject: `${key}.${contribKey}`,
1918
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
1919
+ errors
1920
+ })
1912
1921
  }),
1913
1922
  data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
1914
1923
  });
@@ -1920,9 +1929,9 @@ var annotationFieldUnknownAnalyzer = {
1920
1929
  analyzerId: ID9,
1921
1930
  severity: "warn",
1922
1931
  nodeIds: [node.path],
1923
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
1924
- path: node.path,
1925
- key
1932
+ message: formatFinding({
1933
+ subject: key,
1934
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey)
1926
1935
  }),
1927
1936
  data: { surface: "root", key }
1928
1937
  });
@@ -1979,12 +1988,15 @@ function collectPluginIds(contributions) {
1979
1988
  // plugins/core/analyzers/annotation-orphan/text.ts
1980
1989
  var ANNOTATION_ORPHAN_TEXTS = {
1981
1990
  /**
1982
- * Compact finding grammar: line 1 = the orphan sidecar file, line 2
1983
- * = the diagnosis. The expected markdown path IS the finding's
1984
- * `nodeIds[0]` (the issue files under the path the sidecar points
1985
- * at), so it never appears in the message.
1991
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
1992
+ * wraps it with the backtick subject (the orphan sidecar file); the
1993
+ * expected markdown path IS the finding's `nodeIds[0]`, so it never
1994
+ * appears in the message. The remediation hint moves to
1995
+ * `Issue.fix.summary` below.
1986
1996
  */
1987
- message: "{{sidecarPath}}:\nOrphan sidecar; no matching markdown node."
1997
+ message: "Orphan sidecar; no matching markdown node",
1998
+ /** Remediation hint surfaced via `Issue.fix.summary`. */
1999
+ fixSummary: "Run `sm sidecar prune` to remove orphan sidecars."
1988
2000
  };
1989
2001
 
1990
2002
  // plugins/core/analyzers/annotation-orphan/index.ts
@@ -2005,10 +2017,11 @@ var annotationOrphanAnalyzer = {
2005
2017
  analyzerId: ID10,
2006
2018
  severity: "warn",
2007
2019
  nodeIds: [expectedMdRelative],
2008
- message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
2009
- sidecarPath: orphan.relativePath,
2010
- expectedMdPath: orphan.expectedMdPath
2020
+ message: formatFinding({
2021
+ subject: orphan.relativePath,
2022
+ body: tx(ANNOTATION_ORPHAN_TEXTS.message)
2011
2023
  }),
2024
+ fix: { summary: tx(ANNOTATION_ORPHAN_TEXTS.fixSummary) },
2012
2025
  data: {
2013
2026
  sidecarPath: orphan.relativePath,
2014
2027
  expectedMdPath: orphan.expectedMdPath
@@ -2021,14 +2034,17 @@ var annotationOrphanAnalyzer = {
2021
2034
 
2022
2035
  // plugins/core/analyzers/annotation-stale/text.ts
2023
2036
  var ANNOTATION_STALE_TEXTS = {
2024
- // Compact finding grammar: the affected node is the finding's own
2025
- // node, so its path never appears in the message.
2037
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
2038
+ // emits no subject (the affected node IS the finding's own node); the
2039
+ // remediation hint moves to `Issue.fix.summary` below.
2026
2040
  /** body changed since last bump */
2027
- bodyDrift: "Sidecar `.sm` is stale: body changed since last bump.",
2041
+ bodyDrift: "Sidecar stale; body changed since last bump",
2028
2042
  /** frontmatter changed since last bump */
2029
- frontmatterDrift: "Sidecar `.sm` is stale: frontmatter changed since last bump.",
2043
+ frontmatterDrift: "Sidecar stale; frontmatter changed since last bump",
2030
2044
  /** both body and frontmatter changed */
2031
- bothDrift: "Sidecar `.sm` is stale: body and frontmatter changed since last bump.",
2045
+ bothDrift: "Sidecar stale; body and frontmatter changed since last bump",
2046
+ /** Remediation hint surfaced via `Issue.fix.summary`. */
2047
+ fixSummary: "Run `sm bump <path>` to refresh the sidecar.",
2032
2048
  // Tooltips for the `card.footer.right` clock chip emitted alongside
2033
2049
  // the issue. Lists only the drifted face(s), in-sync faces are
2034
2050
  // omitted so the operator immediately sees what's modified without
@@ -2073,7 +2089,8 @@ var annotationStaleAnalyzer = {
2073
2089
  analyzerId: ID11,
2074
2090
  severity: "info",
2075
2091
  nodeIds: [node.path],
2076
- message: messageFor(status, node.path),
2092
+ message: formatFinding({ body: messageFor(status) }),
2093
+ fix: { summary: tx(ANNOTATION_STALE_TEXTS.fixSummary) },
2077
2094
  data: { status }
2078
2095
  });
2079
2096
  ctx.emitContribution(node.path, staleIcon, {
@@ -2093,14 +2110,14 @@ function staleStatus(overlay) {
2093
2110
  if (status === void 0 || status === null || status === "fresh") return null;
2094
2111
  return status;
2095
2112
  }
2096
- function messageFor(status, path) {
2113
+ function messageFor(status) {
2097
2114
  switch (status) {
2098
2115
  case "stale-body":
2099
- return tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path });
2116
+ return tx(ANNOTATION_STALE_TEXTS.bodyDrift);
2100
2117
  case "stale-frontmatter":
2101
- return tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path });
2118
+ return tx(ANNOTATION_STALE_TEXTS.frontmatterDrift);
2102
2119
  case "stale-both":
2103
- return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2120
+ return tx(ANNOTATION_STALE_TEXTS.bothDrift);
2104
2121
  }
2105
2122
  }
2106
2123
  function tooltipFor(status) {
@@ -2127,6 +2144,92 @@ var contributionOrphanAnalyzer = {
2127
2144
  }
2128
2145
  };
2129
2146
 
2147
+ // plugins/core/analyzers/extractor-collision/text.ts
2148
+ var EXTRACTOR_COLLISION_TEXTS = {
2149
+ /**
2150
+ * Per-Signal warn issue: two extractors detected something at
2151
+ * overlapping byte ranges within the same node and the resolver
2152
+ * dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
2153
+ * reason so the operator can understand why a candidate edge did NOT
2154
+ * become a Link (e.g. a `[link](path)` with `@path` inside the bracket
2155
+ * text: markdown-link wins and the at-directive silently disappears
2156
+ * without this warning).
2157
+ */
2158
+ message: "Overlap collision; {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}, only the winning edge persists",
2159
+ /**
2160
+ * Remediation hint for the range-overlap rejection, surfaced via
2161
+ * `Issue.fix.summary`. Not autofixable: the rule cannot tell which
2162
+ * detection the author meant, so it offers the two resolutions
2163
+ * (rephrase one token, or accept the winner).
2164
+ */
2165
+ rejectedFixSummary: "Rephrase one of the overlapping tokens, or accept the winner."
2166
+ };
2167
+
2168
+ // plugins/core/analyzers/extractor-collision/index.ts
2169
+ var ID13 = "extractor-collision";
2170
+ function signalLines(signal) {
2171
+ return signal.range && typeof signal.range.line === "number" ? [signal.range.line] : void 0;
2172
+ }
2173
+ var extractorCollisionAnalyzer = {
2174
+ id: ID13,
2175
+ pluginId: CORE_PLUGIN_ID,
2176
+ kind: "analyzer",
2177
+ description: "Reports when two extractors detect something at the same span of body text and the resolver drops one.",
2178
+ mode: "deterministic",
2179
+ evaluate(ctx) {
2180
+ const signals = ctx.signals;
2181
+ if (!signals || signals.length === 0) return [];
2182
+ const issues = [];
2183
+ for (const signal of signals) {
2184
+ const issue = makeIssue(signal);
2185
+ if (issue) issues.push(issue);
2186
+ }
2187
+ return issues;
2188
+ }
2189
+ };
2190
+ function makeIssue(signal) {
2191
+ const resolution = signal.resolution;
2192
+ if (!resolution || resolution.outcome !== "rejected" || !resolution.rejectedBy) return null;
2193
+ const winner = resolution.rejectedBy;
2194
+ const loserCandidate = signal.candidates[resolution.winnerIndex ?? 0];
2195
+ const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
2196
+ const winnerRange = `${winner.range.start}-${winner.range.end}`;
2197
+ return {
2198
+ analyzerId: ID13,
2199
+ severity: "warn",
2200
+ nodeIds: [signal.source],
2201
+ message: formatFinding({
2202
+ subject: signal.raw,
2203
+ lines: signalLines(signal),
2204
+ body: tx(EXTRACTOR_COLLISION_TEXTS.message, {
2205
+ loserExtractor: loserCandidate.extractorId,
2206
+ loserRange,
2207
+ winnerExtractor: winner.extractorId,
2208
+ winnerRange,
2209
+ reason: winner.reason
2210
+ })
2211
+ }),
2212
+ fix: { summary: tx(EXTRACTOR_COLLISION_TEXTS.rejectedFixSummary) },
2213
+ data: {
2214
+ loser: {
2215
+ extractorId: loserCandidate.extractorId,
2216
+ raw: signal.raw,
2217
+ range: signal.range ?? null,
2218
+ candidate: {
2219
+ kind: loserCandidate.kind,
2220
+ target: loserCandidate.target,
2221
+ confidence: loserCandidate.confidence
2222
+ }
2223
+ },
2224
+ winner: {
2225
+ extractorId: winner.extractorId,
2226
+ range: winner.range
2227
+ },
2228
+ reason: winner.reason
2229
+ }
2230
+ };
2231
+ }
2232
+
2130
2233
  // plugins/core/analyzers/issue-counter/text.ts
2131
2234
  var ISSUE_COUNTER_TEXTS = {
2132
2235
  errorTooltipSingle: "1 error",
@@ -2136,7 +2239,7 @@ var ISSUE_COUNTER_TEXTS = {
2136
2239
  };
2137
2240
 
2138
2241
  // plugins/core/analyzers/issue-counter/index.ts
2139
- var ID13 = "issue-counter";
2242
+ var ID14 = "issue-counter";
2140
2243
  var warnCount = {
2141
2244
  slot: "card.footer.right",
2142
2245
  icon: "pi-exclamation-triangle",
@@ -2172,7 +2275,7 @@ function emitTierChips(ctx, ref, severity, counts, singleTooltip, manyTooltip) {
2172
2275
  }
2173
2276
  }
2174
2277
  var issueCounterAnalyzer = {
2175
- id: ID13,
2278
+ id: ID14,
2176
2279
  pluginId: CORE_PLUGIN_ID,
2177
2280
  kind: "analyzer",
2178
2281
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
@@ -2203,121 +2306,6 @@ var issueCounterAnalyzer = {
2203
2306
  }
2204
2307
  };
2205
2308
 
2206
- // plugins/core/analyzers/job-file-orphan/text.ts
2207
- var JOB_FILE_ORPHAN_TEXTS = {
2208
- /**
2209
- * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
2210
- * row references it. Compact finding grammar: the file IS the
2211
- * finding's own node, so its path never appears in the message.
2212
- */
2213
- message: "Orphan job file; not referenced by any job. Run `sm job prune --orphan-files` to remove it."
2214
- };
2215
-
2216
- // plugins/core/analyzers/job-file-orphan/index.ts
2217
- var ID14 = "job-file-orphan";
2218
- var jobFileOrphanAnalyzer = {
2219
- id: ID14,
2220
- pluginId: CORE_PLUGIN_ID,
2221
- kind: "analyzer",
2222
- description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
2223
- mode: "deterministic",
2224
- evaluate(ctx) {
2225
- const orphans = ctx.orphanJobFiles;
2226
- if (!orphans || orphans.length === 0) return [];
2227
- const issues = [];
2228
- for (const filePath of orphans) {
2229
- issues.push({
2230
- analyzerId: ID14,
2231
- severity: "warn",
2232
- nodeIds: [filePath],
2233
- message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
2234
- data: { filePath }
2235
- });
2236
- }
2237
- return issues;
2238
- }
2239
- };
2240
-
2241
- // plugins/core/analyzers/link-conflict/text.ts
2242
- var LINK_CONFLICT_TEXTS = {
2243
- /**
2244
- * Compact finding grammar: line 1 = the disputed target, line 2 =
2245
- * the short diagnosis. The source is the finding's own node, so it
2246
- * never appears in the message.
2247
- */
2248
- message: "{{target}}:\nDetectors disagree on link kind ({{kindList}})."
2249
- };
2250
-
2251
- // plugins/core/analyzers/link-conflict/index.ts
2252
- var ID15 = "link-conflict";
2253
- var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2254
- var linkConflictAnalyzer = {
2255
- id: ID15,
2256
- pluginId: CORE_PLUGIN_ID,
2257
- kind: "analyzer",
2258
- description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
2259
- mode: "deterministic",
2260
- // Bucket links by (source, target), then per-bucket detect distinct
2261
- // kinds. The branching is intrinsic to the per-bucket conflict
2262
- // detection.
2263
- // eslint-disable-next-line complexity
2264
- evaluate(ctx) {
2265
- const groups = /* @__PURE__ */ new Map();
2266
- for (const link of ctx.links) {
2267
- if (NON_CONFLICTING_KINDS.has(link.kind)) continue;
2268
- const key = `${link.source}\0${link.target}`;
2269
- const bucket = groups.get(key);
2270
- if (bucket) bucket.push(link);
2271
- else groups.set(key, [link]);
2272
- }
2273
- const issues = [];
2274
- for (const [key, links] of groups) {
2275
- if (links.length < 2) continue;
2276
- const kinds = new Set(links.map((l) => l.kind));
2277
- if (kinds.size < 2) continue;
2278
- const variantByKind = /* @__PURE__ */ new Map();
2279
- for (const link of links) {
2280
- const existing = variantByKind.get(link.kind);
2281
- if (existing) {
2282
- for (const src of link.sources) {
2283
- if (!existing.sources.includes(src)) existing.sources.push(src);
2284
- }
2285
- if (rankConfidence(link.confidence) > rankConfidence(existing.confidence)) {
2286
- existing.confidence = link.confidence;
2287
- }
2288
- } else {
2289
- variantByKind.set(link.kind, {
2290
- kind: link.kind,
2291
- sources: [...link.sources],
2292
- confidence: link.confidence
2293
- });
2294
- }
2295
- }
2296
- for (const v of variantByKind.values()) v.sources.sort();
2297
- const variants = [...variantByKind.values()].sort(
2298
- (a, b) => a.kind.localeCompare(b.kind)
2299
- );
2300
- const [source, target] = key.split("\0");
2301
- const kindList = variants.map((v) => v.kind).join(" / ");
2302
- issues.push({
2303
- analyzerId: ID15,
2304
- severity: "warn",
2305
- nodeIds: [source, target],
2306
- message: tx(LINK_CONFLICT_TEXTS.message, {
2307
- source,
2308
- target,
2309
- kindList
2310
- }),
2311
- data: { source, target, variants }
2312
- });
2313
- }
2314
- return issues;
2315
- }
2316
- };
2317
- function rankConfidence(c) {
2318
- return c;
2319
- }
2320
-
2321
2309
  // kernel/util/link-lines.ts
2322
2310
  function isSelfLoop(link) {
2323
2311
  if (link.source === link.target) return true;
@@ -2336,32 +2324,37 @@ function linkLines(link) {
2336
2324
  }
2337
2325
  return [...lines].sort((a, b) => a - b);
2338
2326
  }
2339
- function linkWhere(link, texts) {
2340
- const lines = linkLines(link);
2341
- if (lines.length === 0) return "";
2342
- return tx(lines.length === 1 ? texts.single : texts.plural, {
2343
- lines: lines.join(", ")
2344
- });
2345
- }
2327
+
2328
+ // plugins/core/analyzers/link-counter/text.ts
2329
+ var LINK_COUNTER_TEXTS = {
2330
+ /** Accessible label for the incoming-links chip. */
2331
+ linksInLabel: "incoming links",
2332
+ /** Accessible label for the outgoing-links chip. */
2333
+ linksOutLabel: "outgoing links",
2334
+ /** Tooltip header for the incoming breakdown (first line). */
2335
+ directionIn: "in",
2336
+ /** Tooltip header for the outgoing breakdown (first line). */
2337
+ directionOut: "out"
2338
+ };
2346
2339
 
2347
2340
  // plugins/core/analyzers/link-counter/index.ts
2348
- var ID16 = "link-counter";
2341
+ var ID15 = "link-counter";
2349
2342
  var linksIn = {
2350
2343
  slot: "card.footer.left",
2351
2344
  icon: "pi-download",
2352
- label: "incoming links",
2345
+ label: LINK_COUNTER_TEXTS.linksInLabel,
2353
2346
  emitWhenEmpty: false,
2354
2347
  priority: 10
2355
2348
  };
2356
2349
  var linksOut = {
2357
2350
  slot: "card.footer.left",
2358
2351
  icon: "pi-upload",
2359
- label: "outgoing links",
2352
+ label: LINK_COUNTER_TEXTS.linksOutLabel,
2360
2353
  emitWhenEmpty: false,
2361
2354
  priority: 20
2362
2355
  };
2363
2356
  var linkCounterAnalyzer = {
2364
- id: ID16,
2357
+ id: ID15,
2365
2358
  pluginId: CORE_PLUGIN_ID,
2366
2359
  kind: "analyzer",
2367
2360
  description: "Counts incoming and outgoing links per node.",
@@ -2404,24 +2397,115 @@ function emitChip(ctx, nodePath, ref, direction, byKind) {
2404
2397
  }
2405
2398
  function formatBreakdown(byKind, direction) {
2406
2399
  const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
2407
- return [direction, ...lines].join("\n");
2400
+ const dirLabel = direction === "in" ? LINK_COUNTER_TEXTS.directionIn : LINK_COUNTER_TEXTS.directionOut;
2401
+ return [dirLabel, ...lines].join("\n");
2402
+ }
2403
+
2404
+ // plugins/core/analyzers/link-kind-conflict/text.ts
2405
+ var LINK_KIND_CONFLICT_TEXTS = {
2406
+ /**
2407
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
2408
+ * wraps it with the backtick subject (the disputed target); the source
2409
+ * is the finding's own node, so it never appears in the message.
2410
+ */
2411
+ message: "Conflicting link kind; detectors disagree ({{kindList}})",
2412
+ /**
2413
+ * Remediation hint surfaced via `Issue.fix.summary`. Not autofixable:
2414
+ * the rule cannot tell which kind the author meant, so it offers the
2415
+ * two valid resolutions (drop one source, or accept the overlap on
2416
+ * purpose). Mirrors the `warn`-severity `link-self-loop` hint shape.
2417
+ */
2418
+ fixSummary: "Remove one of the conflicting sources to settle on a single kind, or ignore the conflict deliberately."
2419
+ };
2420
+
2421
+ // plugins/core/analyzers/link-kind-conflict/index.ts
2422
+ var ID16 = "link-kind-conflict";
2423
+ var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2424
+ var linkKindConflictAnalyzer = {
2425
+ id: ID16,
2426
+ pluginId: CORE_PLUGIN_ID,
2427
+ kind: "analyzer",
2428
+ description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
2429
+ mode: "deterministic",
2430
+ // Bucket links by (source, target), then per-bucket detect distinct
2431
+ // kinds. The branching is intrinsic to the per-bucket conflict
2432
+ // detection.
2433
+ // eslint-disable-next-line complexity
2434
+ evaluate(ctx) {
2435
+ const groups = /* @__PURE__ */ new Map();
2436
+ for (const link of ctx.links) {
2437
+ if (NON_CONFLICTING_KINDS.has(link.kind)) continue;
2438
+ const key = `${link.source}\0${link.target}`;
2439
+ const bucket = groups.get(key);
2440
+ if (bucket) bucket.push(link);
2441
+ else groups.set(key, [link]);
2442
+ }
2443
+ const issues = [];
2444
+ for (const [key, links] of groups) {
2445
+ if (links.length < 2) continue;
2446
+ const kinds = new Set(links.map((l) => l.kind));
2447
+ if (kinds.size < 2) continue;
2448
+ const variantByKind = /* @__PURE__ */ new Map();
2449
+ for (const link of links) {
2450
+ const existing = variantByKind.get(link.kind);
2451
+ if (existing) {
2452
+ for (const src of link.sources) {
2453
+ if (!existing.sources.includes(src)) existing.sources.push(src);
2454
+ }
2455
+ if (rankConfidence(link.confidence) > rankConfidence(existing.confidence)) {
2456
+ existing.confidence = link.confidence;
2457
+ }
2458
+ } else {
2459
+ variantByKind.set(link.kind, {
2460
+ kind: link.kind,
2461
+ sources: [...link.sources],
2462
+ confidence: link.confidence
2463
+ });
2464
+ }
2465
+ }
2466
+ for (const v of variantByKind.values()) v.sources.sort();
2467
+ const variants = [...variantByKind.values()].sort(
2468
+ (a, b) => a.kind.localeCompare(b.kind)
2469
+ );
2470
+ const [source, target] = key.split("\0");
2471
+ const kindList = variants.map((v) => v.kind).join(" / ");
2472
+ issues.push({
2473
+ analyzerId: ID16,
2474
+ severity: "warn",
2475
+ nodeIds: [source, target],
2476
+ message: formatFinding({
2477
+ subject: target,
2478
+ body: tx(LINK_KIND_CONFLICT_TEXTS.message, {
2479
+ kindList
2480
+ })
2481
+ }),
2482
+ fix: { summary: tx(LINK_KIND_CONFLICT_TEXTS.fixSummary) },
2483
+ data: { source, target, variants }
2484
+ });
2485
+ }
2486
+ return issues;
2487
+ }
2488
+ };
2489
+ function rankConfidence(c) {
2490
+ return c;
2408
2491
  }
2409
2492
 
2410
2493
  // plugins/core/analyzers/link-self-loop/text.ts
2411
2494
  var LINK_SELF_LOOP_TEXTS = {
2412
2495
  /**
2413
- * Per-edge warn: a node body references itself via the slash /
2496
+ * Per-edge warn body: a node body references itself via the slash /
2414
2497
  * at-directive / markdown-link surface (most commonly because the
2415
2498
  * file's heading IS the invocation token, e.g. `# /deploy` inside
2416
2499
  * `commands/deploy.md`). The link is structurally valid but rarely
2417
2500
  * the operator's intent; UI consumers MAY hide it by default and
2418
2501
  * surface a count.
2419
2502
  */
2420
- message: "`{{trigger}}`:\nSelf-reference ({{kind}}{{where}}); typically the file's own heading or label. Remove the token or ignore deliberately.",
2421
- /** Location suffix inside the kind parens, one detection site. */
2422
- whereSingle: ", line {{lines}}",
2423
- /** Location suffix inside the kind parens, several detection sites. */
2424
- wherePlural: ", lines {{lines}}"
2503
+ message: "Self-reference; the skill/command invokes itself, potential loop",
2504
+ /**
2505
+ * Remediation hint surfaced via `Issue.fix.summary` (the Inspector
2506
+ * renders it under the finding, separate from the diagnosis body).
2507
+ */
2508
+ fixSummary: "Remove the token or ignore the self-reference deliberately."
2425
2509
  };
2426
2510
 
2427
2511
  // plugins/core/analyzers/link-self-loop/index.ts
@@ -2433,7 +2517,6 @@ var linkSelfLoopAnalyzer = {
2433
2517
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
2434
2518
  mode: "deterministic",
2435
2519
  evaluate(ctx) {
2436
- if (ctx.links.length === 0) return [];
2437
2520
  const issues = [];
2438
2521
  for (const link of ctx.links) {
2439
2522
  if (!isSelfLoop(link)) continue;
@@ -2441,14 +2524,12 @@ var linkSelfLoopAnalyzer = {
2441
2524
  analyzerId: ID17,
2442
2525
  severity: "warn",
2443
2526
  nodeIds: [link.source],
2444
- message: tx(LINK_SELF_LOOP_TEXTS.message, {
2445
- trigger: link.trigger?.originalTrigger ?? link.target,
2446
- kind: link.kind,
2447
- where: linkWhere(link, {
2448
- single: LINK_SELF_LOOP_TEXTS.whereSingle,
2449
- plural: LINK_SELF_LOOP_TEXTS.wherePlural
2450
- })
2527
+ message: formatFinding({
2528
+ subject: link.trigger?.originalTrigger ?? link.target,
2529
+ lines: linkLines(link),
2530
+ body: tx(LINK_SELF_LOOP_TEXTS.message)
2451
2531
  }),
2532
+ fix: { summary: tx(LINK_SELF_LOOP_TEXTS.fixSummary) },
2452
2533
  data: {
2453
2534
  target: link.target,
2454
2535
  resolvedTarget: link.resolvedTarget ?? link.target,
@@ -2460,166 +2541,97 @@ var linkSelfLoopAnalyzer = {
2460
2541
  }
2461
2542
  };
2462
2543
 
2463
- // kernel/orchestrator/node-identifiers.ts
2464
- import { posix as pathPosix4 } from "path";
2465
- function deriveNodeIdentifiers(node, kindDescriptor) {
2466
- const sources = kindDescriptor?.identifiers;
2467
- if (!sources || sources.length === 0) return [];
2468
- const out = [];
2469
- for (const source of sources) {
2470
- const raw = readIdentifier(source, node);
2471
- if (!raw) continue;
2472
- const normalised = normalizeTrigger(raw);
2473
- if (normalised) out.push(normalised);
2474
- }
2475
- return out;
2476
- }
2477
- function readIdentifier(source, node) {
2478
- if (source === "frontmatter.name") return readFrontmatterName(node);
2479
- if (source === "filename-basename") return readFilenameBasename(node);
2480
- return readDirname(node);
2481
- }
2482
- function readFrontmatterName(node) {
2483
- const raw = node.frontmatter?.["name"];
2484
- if (typeof raw !== "string") return null;
2485
- return raw.length > 0 ? raw : null;
2486
- }
2487
- function readFilenameBasename(node) {
2488
- const base = pathPosix4.basename(node.path);
2489
- if (!base) return null;
2490
- const ext = pathPosix4.extname(base);
2491
- const stem = ext ? base.slice(0, -ext.length) : base;
2492
- return stem.length > 0 ? stem : null;
2493
- }
2494
- function readDirname(node) {
2495
- const dir = pathPosix4.dirname(node.path);
2496
- if (!dir || dir === "." || dir === "/") return null;
2497
- const base = pathPosix4.basename(dir);
2498
- return base.length > 0 ? base : null;
2499
- }
2544
+ // plugins/core/analyzers/name-collision/text.ts
2545
+ var NAME_COLLISION_TEXTS = {
2546
+ /**
2547
+ * Diagnosis body (`<what>; <why>: <evidence>`). The shared
2548
+ * `formatFinding` helper wraps it with the backtick subject (the
2549
+ * normalised name claimed by two or more nodes). The evidence is the
2550
+ * competing node paths.
2551
+ */
2552
+ message: "Name collision; {{count}} nodes declare the same name: {{paths}}"
2553
+ };
2500
2554
 
2501
- // kernel/orchestrator/lift-resolved-link-confidence.ts
2502
- var RESERVED_TARGET_CONFIDENCE = 0.1;
2503
- var BROKEN_TARGET_CONFIDENCE = 0.5;
2504
- function liftResolvedLinkConfidence(links, nodes, ctx) {
2505
- if (!links.some((l) => l.confidence < 1)) return;
2506
- const indexes = buildIndexes(nodes, ctx);
2507
- for (const link of links) {
2508
- if (link.confidence < 1) applyResolution(link, indexes, ctx);
2509
- }
2510
- }
2511
- function collectBrokenLinks(links, nodes, ctx) {
2512
- const broken = /* @__PURE__ */ new Set();
2513
- if (links.length === 0) return broken;
2514
- const indexes = buildIndexes(nodes, ctx);
2515
- for (const link of links) {
2516
- if (isGenuinelyBroken(link, indexes)) broken.add(link);
2517
- }
2518
- return broken;
2519
- }
2520
- function applyResolution(link, indexes, ctx) {
2521
- const resolution = resolve2(link, indexes, ctx);
2522
- if (resolution === "none") {
2523
- if (isGenuinelyBroken(link, indexes)) {
2524
- link.confidence = Math.min(link.confidence, BROKEN_TARGET_CONFIDENCE);
2525
- }
2526
- return;
2527
- }
2528
- link.resolvedTarget = resolution;
2529
- if (indexes.nodeByPath.get(resolution)?.virtual) return;
2530
- link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2531
- }
2532
- function buildIndexes(nodes, ctx) {
2533
- const byPath3 = /* @__PURE__ */ new Set();
2534
- const byName = /* @__PURE__ */ new Map();
2535
- const nodeByPath = /* @__PURE__ */ new Map();
2536
- for (const node of nodes) {
2537
- byPath3.add(node.path);
2538
- nodeByPath.set(node.path, node);
2539
- indexNode(node, ctx, byName);
2540
- }
2541
- return { byPath: byPath3, byName, nodeByPath };
2542
- }
2543
- function resolve2(link, indexes, ctx) {
2544
- if (indexes.byPath.has(link.target)) return link.target;
2545
- return resolveByName(link, indexes, ctx);
2546
- }
2547
- function isGenuinelyBroken(link, indexes) {
2548
- if (indexes.byPath.has(link.target)) return false;
2549
- const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2550
- if (stripped !== null && indexes.byName.has(stripped)) return false;
2551
- return true;
2552
- }
2553
- function resolveByName(link, indexes, ctx) {
2554
- const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2555
- if (stripped === null) return "none";
2556
- const candidates = indexes.byName.get(stripped);
2557
- if (!candidates?.length) return "none";
2558
- const allowedKinds = lookupAllowedKinds(link, indexes, ctx);
2559
- if (!allowedKinds?.length) return "none";
2560
- const winner = candidates.find((c) => allowedKinds.includes(c.kind));
2561
- return winner ? winner.path : "none";
2562
- }
2563
- function lookupAllowedKinds(link, _indexes, ctx) {
2564
- if (ctx.activeProvider === null) return void 0;
2565
- return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
2566
- }
2567
- function stripTriggerSigil(normalized) {
2568
- if (!normalized) return null;
2569
- const trimmed = normalized.replace(/^[/@]/, "").trim();
2570
- return trimmed.length === 0 ? null : trimmed;
2571
- }
2572
- function indexNode(node, ctx, byName) {
2573
- const kindDescriptor = ctx.kindRegistry.get(kindKey(node));
2574
- const normalised = deriveNodeIdentifiers(node, kindDescriptor);
2575
- for (const name of normalised) {
2576
- const entry = { kind: node.kind, path: node.path };
2577
- const bucket = byName.get(name);
2578
- if (bucket) {
2579
- bucket.push(entry);
2580
- } else {
2581
- byName.set(name, [entry]);
2555
+ // plugins/core/analyzers/name-collision/index.ts
2556
+ var ID18 = "name-collision";
2557
+ var nameCollisionAnalyzer = {
2558
+ id: ID18,
2559
+ pluginId: CORE_PLUGIN_ID,
2560
+ kind: "analyzer",
2561
+ mode: "deterministic",
2562
+ description: "Flags two or more nodes that declare the same resolvable `name`.",
2563
+ // Pure projector of `ctx.nameCollisions` (computed once by the
2564
+ // orchestrator from the kind registry). One `error` per colliding name.
2565
+ evaluate(ctx) {
2566
+ const collisions = ctx.nameCollisions;
2567
+ if (!collisions || collisions.size === 0) return [];
2568
+ const issues = [];
2569
+ for (const [name, claims] of collisions) {
2570
+ const paths = claims.map((c) => c.path);
2571
+ issues.push({
2572
+ analyzerId: ID18,
2573
+ severity: "error",
2574
+ nodeIds: paths,
2575
+ message: formatFinding({
2576
+ subject: name,
2577
+ body: tx(NAME_COLLISION_TEXTS.message, {
2578
+ count: paths.length,
2579
+ paths: paths.join(", ")
2580
+ })
2581
+ }),
2582
+ data: {
2583
+ name,
2584
+ claims: claims.map((c) => ({ path: c.path, kind: c.kind }))
2585
+ }
2586
+ });
2582
2587
  }
2588
+ return issues;
2583
2589
  }
2584
- }
2585
- function kindKey(node) {
2586
- return `${node.provider}/${node.kind}`;
2587
- }
2590
+ };
2591
+
2592
+ // kernel/orchestrator/confidence-constants.ts
2593
+ var RESERVED_PENALTY = 0.9;
2594
+ var BROKEN_PENALTY = 0.5;
2588
2595
 
2589
2596
  // plugins/core/analyzers/name-reserved/text.ts
2590
2597
  var NAME_RESERVED_TEXTS = {
2591
2598
  /**
2592
- * Target-side message: emitted on the user file that collides with
2593
- * a runtime built-in. Same wording skill-map shipped before the
2594
- * source-side link finding landed.
2599
+ * Target-side body (`<what>; <why>`): emitted on the user file that
2600
+ * collides with a runtime built-in. The shared `formatFinding` helper
2601
+ * adds no subject (the offending node IS the finding's own node); the
2602
+ * remediation hint moves to `Issue.fix.summary` below.
2595
2603
  */
2596
- message: "Name collision: this {{kind}} name is already used by the {{provider}} runtime built-in, which shadows this file. Rename the file or its `frontmatter.name`.",
2604
+ message: "Reserved name; this {{kind}} name is shadowed by the {{provider}} runtime built-in",
2605
+ /** Remediation hint for the target-side finding. */
2606
+ fixSummary: "Rename the file or its frontmatter.name.",
2597
2607
  /**
2598
- * Source-side message: emitted on the node that AUTHORED a link
2599
- * whose target resolves to a reserved name. Explains WHY the link's
2600
- * confidence dropped to `RESERVED_TARGET_CONFIDENCE` (today `0.1`):
2601
- * the kernel saw the target match a runtime built-in and downgraded
2602
- * the edge so the operator notices.
2608
+ * Source-side body (`<what>; <why>`): emitted on the node that
2609
+ * AUTHORED a link whose target resolves to a reserved name. Reports the
2610
+ * fact (the runtime built-in shadows this edge); it deliberately does
2611
+ * NOT assert a confidence number, since the value is owned by the
2612
+ * `score`-phase scorers and may vary or be absent. The shared
2613
+ * `formatFinding` helper wraps it with the backtick target subject and
2614
+ * the `L<line>:` location prefix.
2603
2615
  */
2604
- linkMessage: "{{target}}:\nName collision: resolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; the built-in wins, so this edge drops to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2605
- /** Location suffix after the built-in parens, one detection site. */
2606
- whereSingle: " (line {{lines}})",
2607
- /** Location suffix after the built-in parens, several detection sites. */
2608
- wherePlural: " (lines {{lines}})"
2616
+ linkMessage: "Reserved name; resolves to the {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`), the built-in shadows this edge",
2617
+ /** Remediation hint for the source-side finding. */
2618
+ linkFixSummary: "Rename the target file or its frontmatter.name."
2609
2619
  };
2610
2620
 
2611
2621
  // plugins/core/analyzers/name-reserved/index.ts
2612
- var ID18 = "name-reserved";
2622
+ var ID19 = "name-reserved";
2613
2623
  var nameReservedAnalyzer = {
2614
- id: ID18,
2624
+ id: ID19,
2615
2625
  pluginId: CORE_PLUGIN_ID,
2616
2626
  kind: "analyzer",
2617
2627
  description: "Flags two kinds of reserved-name collision: a file whose name shadows a built-in command of the active runtime, and a link that resolves to one of those reserved names.",
2618
2628
  mode: "deterministic",
2629
+ phase: "score",
2619
2630
  // eslint-disable-next-line complexity
2620
2631
  evaluate(ctx) {
2621
2632
  const reserved = ctx.reservedNodePaths;
2622
2633
  if (!reserved || reserved.size === 0) return [];
2634
+ const adjust = ctx.adjustConfidence;
2623
2635
  const byPath3 = /* @__PURE__ */ new Map();
2624
2636
  for (const node of ctx.nodes) byPath3.set(node.path, node);
2625
2637
  const issues = [];
@@ -2627,35 +2639,41 @@ var nameReservedAnalyzer = {
2627
2639
  const node = byPath3.get(path);
2628
2640
  if (!node) continue;
2629
2641
  issues.push({
2630
- analyzerId: ID18,
2642
+ analyzerId: ID19,
2631
2643
  severity: "warn",
2632
2644
  nodeIds: [node.path],
2633
- message: tx(NAME_RESERVED_TEXTS.message, {
2634
- path: node.path,
2635
- provider: node.provider,
2636
- kind: node.kind
2645
+ message: formatFinding({
2646
+ body: tx(NAME_RESERVED_TEXTS.message, {
2647
+ provider: node.provider,
2648
+ kind: node.kind
2649
+ })
2637
2650
  }),
2651
+ fix: { summary: tx(NAME_RESERVED_TEXTS.fixSummary) },
2638
2652
  data: { provider: node.provider, kind: node.kind, surface: "target" }
2639
2653
  });
2640
2654
  }
2641
2655
  for (const link of ctx.links) {
2642
- if (link.confidence !== RESERVED_TARGET_CONFIDENCE) continue;
2643
2656
  const reservedPath = link.resolvedTarget;
2644
2657
  if (!reservedPath || !reserved.has(reservedPath)) continue;
2645
2658
  const reservedNode = byPath3.get(reservedPath);
2646
2659
  if (!reservedNode) continue;
2660
+ if (adjust) {
2661
+ adjust(link, { kind: "delta", value: -RESERVED_PENALTY });
2662
+ }
2647
2663
  issues.push({
2648
- analyzerId: ID18,
2664
+ analyzerId: ID19,
2649
2665
  severity: "warn",
2650
2666
  nodeIds: [link.source],
2651
- message: tx(NAME_RESERVED_TEXTS.linkMessage, {
2652
- target: link.target,
2653
- provider: reservedNode.provider,
2654
- reservedKind: reservedNode.kind,
2655
- reservedPath: reservedNode.path,
2656
- confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2),
2657
- where: linkWhereSuffix(link)
2667
+ message: formatFinding({
2668
+ subject: link.target,
2669
+ lines: linkLines(link),
2670
+ body: tx(NAME_RESERVED_TEXTS.linkMessage, {
2671
+ provider: reservedNode.provider,
2672
+ reservedKind: reservedNode.kind,
2673
+ reservedPath: reservedNode.path
2674
+ })
2658
2675
  }),
2676
+ fix: { summary: tx(NAME_RESERVED_TEXTS.linkFixSummary) },
2659
2677
  data: {
2660
2678
  target: link.target,
2661
2679
  kind: link.kind,
@@ -2669,15 +2687,13 @@ var nameReservedAnalyzer = {
2669
2687
  return issues;
2670
2688
  }
2671
2689
  };
2672
- function linkWhereSuffix(link) {
2673
- return linkWhere(link, {
2674
- single: NAME_RESERVED_TEXTS.whereSingle,
2675
- plural: NAME_RESERVED_TEXTS.wherePlural
2676
- });
2677
- }
2678
2690
 
2679
2691
  // plugins/core/analyzers/node-stability/text.ts
2680
2692
  var NODE_STABILITY_TEXTS = {
2693
+ /** Issue body (`<what>; <why>`) for an experimental-marked node. */
2694
+ experimental: "Marked experimental; API may change",
2695
+ /** Issue body (`<what>; <why>`) for a deprecated-marked node. */
2696
+ deprecated: "Marked deprecated; avoid in new code",
2681
2697
  /** Label of the inspector action button that sets the lifecycle stage. */
2682
2698
  setLabel: "Set stability",
2683
2699
  /** Prompt label for the enum-pick stability input. */
@@ -2691,7 +2707,7 @@ var NODE_STABILITY_TEXTS = {
2691
2707
  };
2692
2708
 
2693
2709
  // plugins/core/analyzers/node-stability/index.ts
2694
- var ID19 = "node-stability";
2710
+ var ID20 = "node-stability";
2695
2711
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2696
2712
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2697
2713
  var experimental = {
@@ -2713,7 +2729,7 @@ var setStabilityButton = {
2713
2729
  priority: 15
2714
2730
  };
2715
2731
  var nodeStabilityAnalyzer = {
2716
- id: ID19,
2732
+ id: ID20,
2717
2733
  pluginId: CORE_PLUGIN_ID,
2718
2734
  kind: "analyzer",
2719
2735
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
@@ -2732,10 +2748,10 @@ var nodeStabilityAnalyzer = {
2732
2748
  tooltip: EXPERIMENTAL_TOOLTIP
2733
2749
  });
2734
2750
  issues.push({
2735
- analyzerId: ID19,
2751
+ analyzerId: ID20,
2736
2752
  severity: "info",
2737
2753
  nodeIds: [node.path],
2738
- message: `Node '${node.path}' is marked experimental: API may change.`,
2754
+ message: formatFinding({ body: tx(NODE_STABILITY_TEXTS.experimental) }),
2739
2755
  data: { stability }
2740
2756
  });
2741
2757
  } else if (stability === "deprecated") {
@@ -2745,10 +2761,10 @@ var nodeStabilityAnalyzer = {
2745
2761
  severity: "warn"
2746
2762
  });
2747
2763
  issues.push({
2748
- analyzerId: ID19,
2764
+ analyzerId: ID20,
2749
2765
  severity: "warn",
2750
2766
  nodeIds: [node.path],
2751
- message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
2767
+ message: formatFinding({ body: tx(NODE_STABILITY_TEXTS.deprecated) }),
2752
2768
  data: { stability }
2753
2769
  });
2754
2770
  }
@@ -2794,17 +2810,17 @@ function emitSetStabilityButton(ctx, nodePath, current) {
2794
2810
  // plugins/core/analyzers/node-superseded/text.ts
2795
2811
  var NODE_SUPERSEDED_TEXTS = {
2796
2812
  /**
2797
- * Compact finding grammar: line 1 = the superseding artifact, line
2798
- * 2 = what it means. The superseded node is the finding's own node,
2799
- * so its path never appears in the message.
2813
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
2814
+ * wraps it with the backtick subject (the superseding artifact); the
2815
+ * superseded node is the finding's own node, so its path never appears.
2800
2816
  */
2801
- message: "{{supersededBy}}:\nSupersedes this node."
2817
+ message: "Superseded; a newer node supersedes this one"
2802
2818
  };
2803
2819
 
2804
2820
  // plugins/core/analyzers/node-superseded/index.ts
2805
- var ID20 = "node-superseded";
2821
+ var ID21 = "node-superseded";
2806
2822
  var nodeSupersededAnalyzer = {
2807
- id: ID20,
2823
+ id: ID21,
2808
2824
  pluginId: CORE_PLUGIN_ID,
2809
2825
  kind: "analyzer",
2810
2826
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
@@ -2821,12 +2837,12 @@ var nodeSupersededAnalyzer = {
2821
2837
  const supersededBy = pickSupersededBy(node);
2822
2838
  if (supersededBy === null) continue;
2823
2839
  issues.push({
2824
- analyzerId: ID20,
2840
+ analyzerId: ID21,
2825
2841
  severity: "info",
2826
2842
  nodeIds: [node.path],
2827
- message: tx(NODE_SUPERSEDED_TEXTS.message, {
2828
- path: node.path,
2829
- supersededBy
2843
+ message: formatFinding({
2844
+ subject: supersededBy,
2845
+ body: tx(NODE_SUPERSEDED_TEXTS.message)
2830
2846
  }),
2831
2847
  data: { supersededBy }
2832
2848
  });
@@ -2845,22 +2861,27 @@ function pickSupersededBy(node) {
2845
2861
  }
2846
2862
 
2847
2863
  // plugins/core/analyzers/reference-broken/index.ts
2848
- import { resolve as resolve3 } from "path";
2864
+ import { resolve as resolve2 } from "path";
2849
2865
 
2850
2866
  // plugins/core/analyzers/reference-broken/text.ts
2851
2867
  var REFERENCE_BROKEN_TEXTS = {
2852
2868
  /**
2853
- * Compact finding grammar: line 1 = the unresolved target, line 2 =
2854
- * the short diagnosis plus WHERE the reference sits (`{{where}}` is
2855
- * the pre-rendered location suffix below, or empty when the link
2856
- * carries no line info). The source is the finding's own node, so it
2857
- * never appears in the message.
2869
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
2870
+ * wraps it with the backtick subject (the unresolved target) and the
2871
+ * `L<line>:` location prefix; the source is the finding's own node, so
2872
+ * it never appears in the message.
2873
+ */
2874
+ message: "Broken {{kindLabel}}; target not found in the graph or on disk",
2875
+ /**
2876
+ * Remediation hint surfaced via `Issue.fix.summary`. Not autofixable:
2877
+ * the rule cannot tell which resolution the author wants. The folder
2878
+ * option maps to `scan.referencePaths` ("Folders for link validation"
2879
+ * in Settings), the rule's own escape hatch: it clears only PATH-style
2880
+ * breaks (the file exists on disk outside the indexed graph). A
2881
+ * trigger-style `/cmd` / `@agent` break is settled by the path/name or
2882
+ * removal options instead.
2858
2883
  */
2859
- message: "{{target}}:\nBroken {{kindLabel}}{{where}}.",
2860
- /** Location suffix, one detection site. */
2861
- whereSingle: " (line {{lines}})",
2862
- /** Location suffix, several detection sites. */
2863
- wherePlural: " (lines {{lines}})",
2884
+ fixSummary: "Fix the path or name, remove the broken link, or add its folder under Folders for link validation.",
2864
2885
  /**
2865
2886
  * Human noun per link kind for the message above. Fallback for an
2866
2887
  * off-catalog kind: `<kind> link` (composed in the analyzer).
@@ -2880,13 +2901,14 @@ var REFERENCE_BROKEN_TEXTS = {
2880
2901
  };
2881
2902
 
2882
2903
  // plugins/core/analyzers/reference-broken/index.ts
2883
- var ID21 = "reference-broken";
2904
+ var ID22 = "reference-broken";
2884
2905
  var referenceBrokenAnalyzer = {
2885
- id: ID21,
2906
+ id: ID22,
2886
2907
  pluginId: CORE_PLUGIN_ID,
2887
2908
  kind: "analyzer",
2888
2909
  description: "Flags arrows pointing at a node not part of the current scan.",
2889
2910
  mode: "deterministic",
2911
+ phase: "score",
2890
2912
  // No `ui` declaration: this analyzer used to emit a per-finding
2891
2913
  // counter chip on `card.footer.right`, but that chip duplicated the
2892
2914
  // aggregate severity counters now owned by `core/issue-counter`. The
@@ -2902,10 +2924,12 @@ var referenceBrokenAnalyzer = {
2902
2924
  const broken = ctx.brokenLinks;
2903
2925
  if (!broken || broken.size === 0) return [];
2904
2926
  const refIndex = buildReferenceIndex(ctx);
2927
+ const adjust = ctx.adjustConfidence;
2905
2928
  const issues = [];
2906
2929
  for (const link of ctx.links) {
2907
2930
  if (!broken.has(link)) continue;
2908
2931
  if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
2932
+ penalizeBrokenConfidence(adjust, link);
2909
2933
  issues.push(buildIssue(link));
2910
2934
  }
2911
2935
  return issues;
@@ -2915,9 +2939,14 @@ function buildReferenceIndex(ctx) {
2915
2939
  if (!ctx.referenceablePaths || ctx.referenceablePaths.size === 0 || !ctx.cwd) return null;
2916
2940
  return { paths: ctx.referenceablePaths, cwd: ctx.cwd };
2917
2941
  }
2942
+ function penalizeBrokenConfidence(adjust, link) {
2943
+ if (adjust) {
2944
+ adjust(link, { kind: "delta", value: -BROKEN_PENALTY });
2945
+ }
2946
+ }
2918
2947
  function buildIssue(link) {
2919
2948
  return {
2920
- analyzerId: ID21,
2949
+ analyzerId: ID22,
2921
2950
  // `error`, not `warn`: a link whose target is not in the scan is a
2922
2951
  // structural defect the operator must notice, and the card chip
2923
2952
  // paints `danger` (red) to match. Per the chip-vs-issue policy in
@@ -2926,14 +2955,14 @@ function buildIssue(link) {
2926
2955
  // and the global error count on the card.
2927
2956
  severity: "error",
2928
2957
  nodeIds: [link.source],
2929
- message: tx(REFERENCE_BROKEN_TEXTS.message, {
2930
- target: link.target,
2931
- kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind }),
2932
- where: linkWhere(link, {
2933
- single: REFERENCE_BROKEN_TEXTS.whereSingle,
2934
- plural: REFERENCE_BROKEN_TEXTS.wherePlural
2958
+ message: formatFinding({
2959
+ subject: link.target,
2960
+ lines: linkLines(link),
2961
+ body: tx(REFERENCE_BROKEN_TEXTS.message, {
2962
+ kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind })
2935
2963
  })
2936
2964
  }),
2965
+ fix: { summary: tx(REFERENCE_BROKEN_TEXTS.fixSummary) },
2937
2966
  data: {
2938
2967
  target: link.target,
2939
2968
  kind: link.kind,
@@ -2943,7 +2972,7 @@ function buildIssue(link) {
2943
2972
  }
2944
2973
  function resolvesViaReferencePaths(link, refIndex) {
2945
2974
  if (!isPathStyleLink(link)) return false;
2946
- return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
2975
+ return refIndex.paths.has(resolve2(refIndex.cwd, link.target));
2947
2976
  }
2948
2977
  function isPathStyleLink(link) {
2949
2978
  const sigil = link.trigger?.normalizedTrigger?.charAt(0);
@@ -2954,29 +2983,36 @@ function isPathStyleLink(link) {
2954
2983
  // plugins/core/analyzers/reference-redundant/text.ts
2955
2984
  var REFERENCE_REDUNDANT_TEXTS = {
2956
2985
  /**
2957
- * Compact finding grammar (subject first, `\n` renders as a line
2958
- * break in the inspector and flattens to a space in `sm check`):
2959
- *
2960
- * <resolvedTarget>:
2961
- * Duplicate reference (2): `references/x.md` (124, 145).
2962
- *
2986
+ * Diagnosis body (`<what>; <why>`). Kind-agnostic wording ("links", not
2987
+ * "reference"): the redundancy can span different link kinds (e.g.
2988
+ * `invokes` + `references` to one node), so the message never names a
2989
+ * single kind. The shared `formatFinding` helper wraps it with the
2990
+ * backtick subject (the resolved target); no `L<line>:` prefix because
2991
+ * the per-occurrence line numbers stay inline in the rendered
2992
+ * occurrence list. Remediation lives in `fix.summary`, not the message.
2963
2993
  * Occurrences are grouped BY TRIGGER: each distinct trigger text
2964
- * appears once with its line numbers collapsed into one paren list.
2965
- * The source node is the finding's own node, so it never appears.
2994
+ * appears once with its line numbers collapsed into one paren list. The
2995
+ * source node is the finding's own node, so it never appears.
2966
2996
  */
2967
- message: "{{resolvedTarget}}:\nDuplicate reference ({{count}}): {{occurrences}}.",
2997
+ message: "Redundant links; the target is reached {{count}} times: {{occurrences}}",
2998
+ /**
2999
+ * Remediation hint surfaced via `Issue.fix.summary`. Phrased as
3000
+ * optional: severity is `info` and keeping multiple forms can be
3001
+ * deliberate, so the hint offers consolidation OR keeping the overlap.
3002
+ */
3003
+ fixSummary: "Consolidate the links into one, or keep the overlap deliberately.",
2968
3004
  /** Inline separator between trigger groups in the message. */
2969
3005
  occurrenceSeparator: ", ",
2970
3006
  /** Per-trigger formatting: the trigger once, its lines grouped. */
2971
- occurrence: "`{{trigger}}` ({{lines}})",
3007
+ occurrence: "`{{trigger}}` (L{{lines}})",
2972
3008
  /** Placeholder for an occurrence whose extractor recorded no line. */
2973
3009
  lineUnknown: "?"
2974
3010
  };
2975
3011
 
2976
3012
  // plugins/core/analyzers/reference-redundant/index.ts
2977
- var ID22 = "reference-redundant";
3013
+ var ID23 = "reference-redundant";
2978
3014
  var referenceRedundantAnalyzer = {
2979
- id: ID22,
3015
+ id: ID23,
2980
3016
  pluginId: CORE_PLUGIN_ID,
2981
3017
  kind: "analyzer",
2982
3018
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
@@ -2999,14 +3035,17 @@ var referenceRedundantAnalyzer = {
2999
3035
  const [source, resolvedTarget] = key.split("\0");
3000
3036
  const flat = flattenOccurrences(links);
3001
3037
  issues.push({
3002
- analyzerId: ID22,
3038
+ analyzerId: ID23,
3003
3039
  severity: "info",
3004
3040
  nodeIds: [source],
3005
- message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
3006
- resolvedTarget,
3007
- count: flat.length,
3008
- occurrences: formatGroupedOccurrences(flat)
3041
+ message: formatFinding({
3042
+ subject: resolvedTarget,
3043
+ body: tx(REFERENCE_REDUNDANT_TEXTS.message, {
3044
+ count: flat.length,
3045
+ occurrences: formatGroupedOccurrences(flat)
3046
+ })
3009
3047
  }),
3048
+ fix: { summary: tx(REFERENCE_REDUNDANT_TEXTS.fixSummary) },
3010
3049
  data: {
3011
3050
  target: resolvedTarget,
3012
3051
  resolvedTarget,
@@ -3069,7 +3108,7 @@ function formatGroupedOccurrences(occurrences) {
3069
3108
 
3070
3109
  // kernel/adapters/schema-validators.ts
3071
3110
  import { readFileSync as readFileSync2 } from "fs";
3072
- import { dirname as dirname2, resolve as resolve4 } from "path";
3111
+ import { dirname as dirname2, resolve as resolve3 } from "path";
3073
3112
  import { createRequire as createRequire2 } from "module";
3074
3113
  import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
3075
3114
 
@@ -3137,14 +3176,14 @@ function buildSchemaValidators() {
3137
3176
  });
3138
3177
  applyAjvFormats(ajv);
3139
3178
  for (const rel of SUPPORTING_SCHEMAS) {
3140
- const file = resolve4(specRoot, rel);
3179
+ const file = resolve3(specRoot, rel);
3141
3180
  if (!existsSyncSafe(file)) continue;
3142
3181
  const schema = JSON.parse(readFileSync2(file, "utf8"));
3143
3182
  ajv.addSchema(schema);
3144
3183
  }
3145
3184
  const validators = /* @__PURE__ */ new Map();
3146
3185
  for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
3147
- const file = resolve4(specRoot, rel);
3186
+ const file = resolve3(specRoot, rel);
3148
3187
  const schema = JSON.parse(readFileSync2(file, "utf8"));
3149
3188
  const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
3150
3189
  validators.set(name, byId ?? ajv.compile(schema));
@@ -3216,7 +3255,7 @@ function buildProviderFrontmatterValidator(providers) {
3216
3255
  allowUnionTypes: true
3217
3256
  });
3218
3257
  applyAjvFormats(ajv);
3219
- const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
3258
+ const baseFile = resolve3(specRoot, "schemas/frontmatter/base.schema.json");
3220
3259
  const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
3221
3260
  ajv.addSchema(baseSchema);
3222
3261
  registerProviderAuxiliarySchemas(ajv, providers);
@@ -3315,16 +3354,25 @@ function existsSyncSafe(path) {
3315
3354
  }
3316
3355
  }
3317
3356
 
3357
+ // kernel/orchestrator/frontmatter-issue-ids.ts
3358
+ var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
3359
+ "frontmatter-invalid",
3360
+ "frontmatter-malformed",
3361
+ "frontmatter-parse-error"
3362
+ ]);
3363
+
3318
3364
  // plugins/core/analyzers/schema-violation/text.ts
3319
3365
  var SCHEMA_VIOLATION_TEXTS = {
3320
- // Compact finding grammar: the affected node (or the link's source)
3321
- // is the finding's own node, so its path never appears.
3322
- /** `Schema validation failed: <errors>` */
3323
- nodeFailure: "Schema validation failed: {{errors}}",
3324
- /** `<target>:\nLink failed schema validation: <errors>` */
3325
- linkFailure: "{{target}}:\nLink failed schema validation: {{errors}}",
3326
- /** `Missing required frontmatter: <missing>.` */
3327
- frontmatterBaseFailure: "Missing required frontmatter: {{missing}}.",
3366
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
3367
+ // owns the subject / location chrome: the node + frontmatter findings
3368
+ // carry no subject (the affected node IS the finding's own node), the
3369
+ // link finding uses the link target as its subject.
3370
+ /** `Schema validation failed; <errors>` */
3371
+ nodeFailure: "Schema validation failed; {{errors}}",
3372
+ /** `<target>` subject + `Link failed schema validation; <errors>` */
3373
+ linkFailure: "Link failed schema validation; {{errors}}",
3374
+ /** `Missing required frontmatter; <missing>` */
3375
+ frontmatterBaseFailure: "Missing required frontmatter; {{missing}}",
3328
3376
  /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
3329
3377
  alertTooltipSingle: "Frontmatter or schema validation failed.",
3330
3378
  /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
@@ -3332,9 +3380,9 @@ var SCHEMA_VIOLATION_TEXTS = {
3332
3380
  };
3333
3381
 
3334
3382
  // plugins/core/analyzers/schema-violation/index.ts
3335
- var ID23 = "schema-violation";
3383
+ var ID24 = "schema-violation";
3336
3384
  var schemaViolationAnalyzer = {
3337
- id: ID23,
3385
+ id: ID24,
3338
3386
  pluginId: CORE_PLUGIN_ID,
3339
3387
  kind: "analyzer",
3340
3388
  description: "Flags nodes or links that violate the project schemas.",
@@ -3355,10 +3403,11 @@ var schemaViolationAnalyzer = {
3355
3403
  const validators = loadSchemaValidators();
3356
3404
  const findings = [];
3357
3405
  const perNode = /* @__PURE__ */ new Map();
3406
+ const kernelFlaggedNodes = collectKernelFlaggedNodes(ctx.accumulatedIssues);
3358
3407
  for (const node of ctx.nodes) {
3359
3408
  const before = findings.length;
3360
3409
  collectNodeFindings(validators, node, findings);
3361
- collectFrontmatterBaseFindings(node, findings);
3410
+ collectFrontmatterBaseFindings(node, findings, kernelFlaggedNodes);
3362
3411
  if (findings.length > before) {
3363
3412
  let worst = "warn";
3364
3413
  for (let i = before; i < findings.length; i++) {
@@ -3385,17 +3434,27 @@ function collectNodeFindings(v, node, out) {
3385
3434
  const result = v.validate("node", toNodeForSchema(node));
3386
3435
  if (result.ok) return;
3387
3436
  out.push({
3388
- analyzerId: ID23,
3437
+ analyzerId: ID24,
3389
3438
  severity: "error",
3390
3439
  nodeIds: [node.path],
3391
- message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
3392
- path: node.path,
3393
- errors: result.errors
3440
+ message: formatFinding({
3441
+ body: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
3442
+ errors: result.errors
3443
+ })
3394
3444
  }),
3395
3445
  data: { target: "node", path: node.path }
3396
3446
  });
3397
3447
  }
3398
- function collectFrontmatterBaseFindings(node, out) {
3448
+ function collectKernelFlaggedNodes(accumulated) {
3449
+ const flagged = /* @__PURE__ */ new Set();
3450
+ for (const issue of accumulated ?? []) {
3451
+ if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
3452
+ for (const id of issue.nodeIds) flagged.add(id);
3453
+ }
3454
+ return flagged;
3455
+ }
3456
+ function collectFrontmatterBaseFindings(node, out, kernelFlagged) {
3457
+ if (kernelFlagged.has(node.path)) return;
3399
3458
  if (node.provider === "markdown") return;
3400
3459
  if (node.bytes.frontmatter === 0) return;
3401
3460
  const fm = node.frontmatter ?? {};
@@ -3404,16 +3463,17 @@ function collectFrontmatterBaseFindings(node, out) {
3404
3463
  if (isMissingStringField(fm, "description")) missing.push("description");
3405
3464
  if (missing.length === 0) return;
3406
3465
  out.push({
3407
- analyzerId: ID23,
3466
+ analyzerId: ID24,
3408
3467
  // `warn` (not `error`) so the default `sm scan` exit code stays
3409
3468
  // 0 even when nodes are missing frontmatter base fields. Strict
3410
3469
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
3411
3470
  // the `frontmatter-invalid` severity policy of the orchestrator.
3412
3471
  severity: "warn",
3413
3472
  nodeIds: [node.path],
3414
- message: tx(SCHEMA_VIOLATION_TEXTS.frontmatterBaseFailure, {
3415
- path: node.path,
3416
- missing: missing.join(", ")
3473
+ message: formatFinding({
3474
+ body: tx(SCHEMA_VIOLATION_TEXTS.frontmatterBaseFailure, {
3475
+ missing: missing.join(", ")
3476
+ })
3417
3477
  }),
3418
3478
  data: { target: "frontmatter", path: node.path, missing }
3419
3479
  });
@@ -3426,13 +3486,14 @@ function collectLinkFindings(v, link, out) {
3426
3486
  const result = v.validate("link", toLinkForSchema(link));
3427
3487
  if (result.ok) return;
3428
3488
  out.push({
3429
- analyzerId: ID23,
3489
+ analyzerId: ID24,
3430
3490
  severity: "error",
3431
3491
  nodeIds: [link.source],
3432
- message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
3433
- source: link.source,
3434
- target: link.target,
3435
- errors: result.errors
3492
+ message: formatFinding({
3493
+ subject: link.target,
3494
+ body: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
3495
+ errors: result.errors
3496
+ })
3436
3497
  }),
3437
3498
  data: { target: "link", source: link.source, to: link.target }
3438
3499
  });
@@ -3453,292 +3514,16 @@ function toNodeForSchema(node) {
3453
3514
  sidecar: node.sidecar ?? void 0
3454
3515
  };
3455
3516
  }
3456
- function toLinkForSchema(link) {
3457
- return {
3458
- source: link.source,
3459
- target: link.target,
3460
- kind: link.kind,
3461
- confidence: link.confidence,
3462
- sources: link.sources,
3463
- trigger: link.trigger ?? void 0,
3464
- location: link.location ?? void 0,
3465
- raw: link.raw ?? void 0
3466
- };
3467
- }
3468
-
3469
- // plugins/core/analyzers/signal-collision/text.ts
3470
- var SIGNAL_COLLISION_TEXTS = {
3471
- /**
3472
- * Per-Signal warn issue: two extractors detected something at
3473
- * overlapping byte ranges within the same node and the resolver
3474
- * dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
3475
- * reason so the operator can understand why a candidate edge did NOT
3476
- * become a Link.
3477
- *
3478
- * Placeholders are deliberately verbose because this is one of the
3479
- * few diagnostic surfaces where the operator may need to disambiguate
3480
- * a confusing graph (e.g. a `[link](path)` followed by `@path` inside
3481
- * the same paragraph, the markdown-link wins and the at-directive
3482
- * silently disappears without this warning).
3483
- */
3484
- message: "`{{loserRaw}}`:\nOverlap collision: {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}; only the winning edge persists.",
3485
- /**
3486
- * Same warn but for the rare case the resolver rejected a Signal
3487
- * because the operator disabled its extractor via
3488
- * `plugins.<id>.extensions.<extId>.enabled`. Phase 4+ stub: today the
3489
- * filter is not wired so this template is unreachable from the
3490
- * resolver; documented now so the analyzer stays forward-compatible
3491
- * with the upcoming filter pass.
3492
- */
3493
- messageExtractorDisabled: "`{{loserRaw}}`:\nDropped: extension `{{extractorId}}` is disabled. Re-enable it in Settings or via `sm plugins enable`.",
3494
- /**
3495
- * Same warn but for the future confidence floor case. Phase 4+ stub:
3496
- * today the resolver materialises every winning candidate regardless
3497
- * of confidence, so this template is unreachable; documented for
3498
- * forward compatibility.
3499
- */
3500
- messageBelowFloor: "`{{loserRaw}}`:\nDropped: confidence {{confidence}} is below the threshold {{threshold}}."
3501
- };
3502
-
3503
- // plugins/core/analyzers/signal-collision/index.ts
3504
- var ID24 = "signal-collision";
3505
- var signalCollisionAnalyzer = {
3506
- id: ID24,
3507
- pluginId: CORE_PLUGIN_ID,
3508
- kind: "analyzer",
3509
- description: "Reports when two extractors fight over the same span of body text, or when a candidate link is dropped (extractor disabled, confidence too low) before it reaches the graph.",
3510
- mode: "deterministic",
3511
- evaluate(ctx) {
3512
- const signals = ctx.signals;
3513
- if (!signals || signals.length === 0) return [];
3514
- const issues = [];
3515
- for (const signal of signals) {
3516
- const issue = makeIssue(signal);
3517
- if (issue) issues.push(issue);
3518
- }
3519
- return issues;
3520
- }
3521
- };
3522
- function makeIssue(signal) {
3523
- const resolution = signal.resolution;
3524
- if (!resolution || resolution.outcome !== "rejected") return null;
3525
- if (resolution.rejectedBy) {
3526
- const winner = resolution.rejectedBy;
3527
- const winnerCandidate = signal.candidates[resolution.winnerIndex ?? 0];
3528
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3529
- const winnerRange = `${winner.range.start}-${winner.range.end}`;
3530
- return {
3531
- analyzerId: ID24,
3532
- severity: "warn",
3533
- nodeIds: [signal.source],
3534
- message: tx(SIGNAL_COLLISION_TEXTS.message, {
3535
- loserExtractor: winnerCandidate.extractorId,
3536
- loserRaw: signal.raw,
3537
- loserRange,
3538
- winnerExtractor: winner.extractorId,
3539
- winnerRange,
3540
- reason: winner.reason
3541
- }),
3542
- data: {
3543
- loser: {
3544
- extractorId: winnerCandidate.extractorId,
3545
- raw: signal.raw,
3546
- range: signal.range ?? null,
3547
- candidate: {
3548
- kind: winnerCandidate.kind,
3549
- target: winnerCandidate.target,
3550
- confidence: winnerCandidate.confidence
3551
- }
3552
- },
3553
- winner: {
3554
- extractorId: winner.extractorId,
3555
- range: winner.range
3556
- },
3557
- reason: winner.reason
3558
- }
3559
- };
3560
- }
3561
- if (resolution.extractorDisabled) {
3562
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3563
- return {
3564
- analyzerId: ID24,
3565
- severity: "warn",
3566
- nodeIds: [signal.source],
3567
- message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
3568
- extractorId: resolution.extractorDisabled.extractorId,
3569
- loserRaw: signal.raw,
3570
- loserRange
3571
- }),
3572
- data: {
3573
- extractorDisabled: resolution.extractorDisabled,
3574
- raw: signal.raw,
3575
- range: signal.range ?? null
3576
- }
3577
- };
3578
- }
3579
- if (resolution.belowFloor) {
3580
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3581
- const topCandidate = signal.candidates[0];
3582
- return {
3583
- analyzerId: ID24,
3584
- severity: "warn",
3585
- nodeIds: [signal.source],
3586
- message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
3587
- loserRaw: signal.raw,
3588
- loserRange,
3589
- confidence: topCandidate.confidence,
3590
- threshold: resolution.belowFloor.threshold
3591
- }),
3592
- data: {
3593
- belowFloor: resolution.belowFloor,
3594
- raw: signal.raw,
3595
- range: signal.range ?? null
3596
- }
3597
- };
3598
- }
3599
- return null;
3600
- }
3601
-
3602
- // plugins/core/analyzers/trigger-collision/text.ts
3603
- var TRIGGER_COLLISION_TEXTS = {
3604
- /**
3605
- * Top-level message when `analyzeTriggerBucket` accumulated exactly one
3606
- * cause part. Used for the advertiser-ambiguous-only, invocation-
3607
- * ambiguous-only, and cross-kind-only branches.
3608
- */
3609
- messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3610
- /**
3611
- * Top-level message when `analyzeTriggerBucket` accumulated two cause
3612
- * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3613
- * The joiner lives inside the template so future locales can adapt it
3614
- * (e.g. `'; y '` in Spanish) without touching the rule code.
3615
- */
3616
- messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3617
- /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3618
- partAdvertisers: "{{count}} advertisers: {{paths}}",
3619
- /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3620
- partInvocations: "{{count}} invocation forms: {{forms}}",
3621
- /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3622
- partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3623
- /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
3624
- partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
3625
- };
3626
-
3627
- // plugins/core/analyzers/trigger-collision/index.ts
3628
- var ID25 = "trigger-collision";
3629
- var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3630
- "command",
3631
- "skill",
3632
- "agent"
3633
- ]);
3634
- var triggerCollisionAnalyzer = {
3635
- id: ID25,
3636
- pluginId: CORE_PLUGIN_ID,
3637
- kind: "analyzer",
3638
- mode: "deterministic",
3639
- description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
3640
- // Two claim-collection passes (advertisement + invocation) feeding
3641
- // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
3642
- // eslint-disable-next-line complexity
3643
- evaluate(ctx) {
3644
- const buckets = /* @__PURE__ */ new Map();
3645
- const push = (key, claim) => {
3646
- const bucket = buckets.get(key) ?? [];
3647
- bucket.push(claim);
3648
- buckets.set(key, bucket);
3649
- };
3650
- for (const node of ctx.nodes) {
3651
- if (!ADVERTISING_KINDS.has(node.kind)) continue;
3652
- const raw = node.frontmatter?.["name"];
3653
- if (typeof raw !== "string" || raw.length === 0) continue;
3654
- const normalized = `/${normalizeTrigger(raw)}`;
3655
- if (normalized === "/") continue;
3656
- push(normalized, {
3657
- kind: "advertiser",
3658
- token: node.path,
3659
- nodeId: node.path,
3660
- canonicalForm: `/${raw}`
3661
- });
3662
- }
3663
- for (const link of ctx.links) {
3664
- const normalized = link.trigger?.normalizedTrigger;
3665
- if (!normalized) continue;
3666
- push(normalized, {
3667
- kind: "invocation",
3668
- token: link.target,
3669
- nodeId: link.source
3670
- });
3671
- }
3672
- const issues = [];
3673
- for (const [normalized, claims] of buckets) {
3674
- const issue = analyzeTriggerBucket(normalized, claims);
3675
- if (issue) issues.push(issue);
3676
- }
3677
- return issues;
3678
- }
3679
- };
3680
- function analyzeTriggerBucket(normalized, claims) {
3681
- const advertiserPaths = [
3682
- ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
3683
- ].sort();
3684
- const invocationTargets = [
3685
- ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
3686
- ].sort();
3687
- const advertisers = claims.filter(
3688
- (c) => c.kind === "advertiser"
3689
- );
3690
- const advertiserAmbiguous = advertiserPaths.length >= 2;
3691
- const invocationAmbiguous = invocationTargets.length >= 2;
3692
- const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
3693
- const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
3694
- const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
3695
- if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
3696
- return null;
3697
- }
3698
- const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
3699
- const parts = [];
3700
- if (advertiserAmbiguous) {
3701
- parts.push(
3702
- tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
3703
- count: advertiserPaths.length,
3704
- paths: advertiserPaths.join(", ")
3705
- })
3706
- );
3707
- }
3708
- if (invocationAmbiguous) {
3709
- parts.push(
3710
- tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
3711
- count: invocationTargets.length,
3712
- forms: invocationTargets.join(", ")
3713
- })
3714
- );
3715
- } else if (crossKindAmbiguous) {
3716
- const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
3717
- parts.push(
3718
- tx(template, {
3719
- forms: nonCanonicalInvocations.join(", "),
3720
- advertiser: advertiserPaths[0]
3721
- })
3722
- );
3723
- }
3724
- const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
3725
- normalized,
3726
- first: parts[0],
3727
- second: parts[1]
3728
- }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
3729
- normalized,
3730
- part: parts[0]
3731
- });
3517
+ function toLinkForSchema(link) {
3732
3518
  return {
3733
- analyzerId: ID25,
3734
- severity: "error",
3735
- nodeIds,
3736
- message,
3737
- data: {
3738
- normalizedTrigger: normalized,
3739
- invocationTargets,
3740
- advertiserPaths
3741
- }
3519
+ source: link.source,
3520
+ target: link.target,
3521
+ kind: link.kind,
3522
+ confidence: link.confidence,
3523
+ sources: link.sources,
3524
+ trigger: link.trigger ?? void 0,
3525
+ location: link.location ?? void 0,
3526
+ raw: link.raw ?? void 0
3742
3527
  };
3743
3528
  }
3744
3529
 
@@ -3770,13 +3555,13 @@ var ASCII_FORMATTER_TEXTS = {
3770
3555
  };
3771
3556
 
3772
3557
  // plugins/core/formatters/ascii/index.ts
3773
- var ID26 = "ascii";
3558
+ var ID25 = "ascii";
3774
3559
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3775
3560
  var asciiFormatter = {
3776
- id: ID26,
3561
+ id: ID25,
3777
3562
  pluginId: CORE_PLUGIN_ID,
3778
3563
  kind: "formatter",
3779
- formatId: ID26,
3564
+ formatId: ID25,
3780
3565
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3781
3566
  // ASCII tree formatter, header + per-kind sections + per-issue
3782
3567
  // section. Each section iterates and renders; splitting per section
@@ -3870,13 +3655,13 @@ function renderSection(out, kind, group) {
3870
3655
  }
3871
3656
 
3872
3657
  // plugins/core/formatters/json/index.ts
3873
- var ID27 = "json";
3658
+ var ID26 = "json";
3874
3659
  var jsonFormatter = {
3875
- id: ID27,
3660
+ id: ID26,
3876
3661
  pluginId: CORE_PLUGIN_ID,
3877
3662
  kind: "formatter",
3878
3663
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
3879
- formatId: ID27,
3664
+ formatId: ID26,
3880
3665
  format(ctx) {
3881
3666
  if (ctx.scanResult !== void 0) {
3882
3667
  return JSON.stringify(ctx.scanResult);
@@ -3891,7 +3676,7 @@ var jsonFormatter = {
3891
3676
 
3892
3677
  // kernel/sidecar/parse.ts
3893
3678
  import { existsSync, readFileSync as readFileSync3 } from "fs";
3894
- import { dirname as dirname3, resolve as resolve5 } from "path";
3679
+ import { dirname as dirname3, resolve as resolve4 } from "path";
3895
3680
  import { createRequire as createRequire3 } from "module";
3896
3681
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
3897
3682
  import yaml from "js-yaml";
@@ -3993,10 +3778,10 @@ function getSidecarValidator() {
3993
3778
  applyAjvFormats(ajv);
3994
3779
  const specRoot = resolveSpecRoot2();
3995
3780
  const annotationsSchema = JSON.parse(
3996
- readFileSync3(resolve5(specRoot, "schemas/annotations.schema.json"), "utf8")
3781
+ readFileSync3(resolve4(specRoot, "schemas/annotations.schema.json"), "utf8")
3997
3782
  );
3998
3783
  const sidecarSchema = JSON.parse(
3999
- readFileSync3(resolve5(specRoot, "schemas/sidecar.schema.json"), "utf8")
3784
+ readFileSync3(resolve4(specRoot, "schemas/sidecar.schema.json"), "utf8")
4000
3785
  );
4001
3786
  ajv.addSchema(annotationsSchema);
4002
3787
  cachedSidecarValidator = ajv.compile(sidecarSchema);
@@ -4023,13 +3808,13 @@ var BUMP_TEXTS = {
4023
3808
  };
4024
3809
 
4025
3810
  // plugins/core/actions/node-bump/index.ts
4026
- var ID28 = "node-bump";
3811
+ var ID27 = "node-bump";
4027
3812
  var bumpButton = {
4028
3813
  slot: "inspector.action.button",
4029
3814
  priority: 10
4030
3815
  };
4031
3816
  var nodeBumpAction = {
4032
- id: ID28,
3817
+ id: ID27,
4033
3818
  pluginId: CORE_PLUGIN_ID,
4034
3819
  kind: "action",
4035
3820
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -4109,9 +3894,9 @@ function pickCurrentVersion(overlay) {
4109
3894
 
4110
3895
  // plugins/core/actions/node-set-stability/index.ts
4111
3896
  var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4112
- var ID29 = "node-set-stability";
3897
+ var ID28 = "node-set-stability";
4113
3898
  var nodeSetStabilityAction = {
4114
- id: ID29,
3899
+ id: ID28,
4115
3900
  pluginId: CORE_PLUGIN_ID,
4116
3901
  kind: "action",
4117
3902
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -4159,13 +3944,13 @@ var TAGS_TEXTS = {
4159
3944
  };
4160
3945
 
4161
3946
  // plugins/core/actions/node-set-tags/index.ts
4162
- var ID30 = "node-set-tags";
3947
+ var ID29 = "node-set-tags";
4163
3948
  var setTagsButton = {
4164
3949
  slot: "inspector.action.button",
4165
3950
  priority: 15
4166
3951
  };
4167
3952
  var nodeSetTagsAction = {
4168
- id: ID30,
3953
+ id: ID29,
4169
3954
  pluginId: CORE_PLUGIN_ID,
4170
3955
  kind: "action",
4171
3956
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
@@ -4242,13 +4027,13 @@ var SUPERSEDE_TEXTS = {
4242
4027
  };
4243
4028
 
4244
4029
  // plugins/core/actions/node-supersede/index.ts
4245
- var ID31 = "node-supersede";
4030
+ var ID30 = "node-supersede";
4246
4031
  var supersedeButton = {
4247
4032
  slot: "inspector.action.button",
4248
4033
  priority: 10
4249
4034
  };
4250
4035
  var nodeSupersedeAction = {
4251
- id: ID31,
4036
+ id: ID30,
4252
4037
  pluginId: CORE_PLUGIN_ID,
4253
4038
  kind: "action",
4254
4039
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
@@ -4536,7 +4321,7 @@ function writeJsonAtomic(path, content) {
4536
4321
  }
4537
4322
 
4538
4323
  // core/paths/db-path.ts
4539
- import { join as join2, resolve as resolve6 } from "path";
4324
+ import { join as join2, resolve as resolve5 } from "path";
4540
4325
 
4541
4326
  // kernel/util/skill-map-paths.ts
4542
4327
  import { join } from "path";
@@ -4564,17 +4349,17 @@ var GITIGNORE_ENTRIES = [
4564
4349
  `${SKILL_MAP_DIR}/${DB_FILENAME}`
4565
4350
  ];
4566
4351
  function resolveDbPath(options) {
4567
- if (options.db) return resolve6(options.db);
4568
- return resolve6(options.cwd, DEFAULT_DB_REL);
4352
+ if (options.db) return resolve5(options.db);
4353
+ return resolve5(options.cwd, DEFAULT_DB_REL);
4569
4354
  }
4570
4355
  function defaultProjectDbPath(ctx) {
4571
- return resolve6(ctx.cwd, DEFAULT_DB_REL);
4356
+ return resolve5(ctx.cwd, DEFAULT_DB_REL);
4572
4357
  }
4573
4358
  function defaultProjectJobsDir(ctx) {
4574
- return resolve6(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);
4359
+ return resolve5(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);
4575
4360
  }
4576
4361
  function defaultProjectPluginsDir(ctx) {
4577
- return resolve6(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);
4362
+ return resolve5(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);
4578
4363
  }
4579
4364
  function defaultDbPath(scopeRoot) {
4580
4365
  return join2(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);
@@ -4823,19 +4608,18 @@ var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, plugi
4823
4608
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: VERSION };
4824
4609
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: VERSION };
4825
4610
  var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: VERSION };
4611
+ var extractorCollisionAnalyzer2 = { ...extractorCollisionAnalyzer, pluginId: "core", version: VERSION };
4826
4612
  var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: VERSION };
4827
- var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: VERSION };
4828
- var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: VERSION };
4829
4613
  var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: VERSION };
4614
+ var linkKindConflictAnalyzer2 = { ...linkKindConflictAnalyzer, pluginId: "core", version: VERSION };
4830
4615
  var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: VERSION };
4616
+ var nameCollisionAnalyzer2 = { ...nameCollisionAnalyzer, pluginId: "core", version: VERSION };
4831
4617
  var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: VERSION };
4832
4618
  var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: VERSION };
4833
4619
  var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: VERSION };
4834
4620
  var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: VERSION };
4835
4621
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4836
4622
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4837
- var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4838
- var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4839
4623
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4840
4624
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
4841
4625
  var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: VERSION };
@@ -4889,19 +4673,18 @@ var builtInPlugins = [
4889
4673
  annotationOrphanAnalyzer2,
4890
4674
  annotationStaleAnalyzer2,
4891
4675
  contributionOrphanAnalyzer2,
4676
+ extractorCollisionAnalyzer2,
4892
4677
  issueCounterAnalyzer2,
4893
- jobFileOrphanAnalyzer2,
4894
- linkConflictAnalyzer2,
4895
4678
  linkCounterAnalyzer2,
4679
+ linkKindConflictAnalyzer2,
4896
4680
  linkSelfLoopAnalyzer2,
4681
+ nameCollisionAnalyzer2,
4897
4682
  nameReservedAnalyzer2,
4898
4683
  nodeStabilityAnalyzer2,
4899
4684
  nodeSupersededAnalyzer2,
4900
4685
  referenceBrokenAnalyzer2,
4901
4686
  referenceRedundantAnalyzer2,
4902
4687
  schemaViolationAnalyzer2,
4903
- signalCollisionAnalyzer2,
4904
- triggerCollisionAnalyzer2,
4905
4688
  asciiFormatter2,
4906
4689
  jsonFormatter2,
4907
4690
  nodeBumpAction2,
@@ -5720,7 +5503,7 @@ import { Command as Command2, Option as Option2 } from "clipanion";
5720
5503
 
5721
5504
  // core/config/helper.ts
5722
5505
  import { homedir as osHomedir } from "os";
5723
- import { isAbsolute, join as join4, resolve as resolve7, sep } from "path";
5506
+ import { isAbsolute, join as join4, resolve as resolve6, sep } from "path";
5724
5507
 
5725
5508
  // kernel/config/loader.ts
5726
5509
  import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
@@ -6138,13 +5921,13 @@ function projectPathExposure(inputs) {
6138
5921
  return { expandsSurface: true, exposedPaths: exposed };
6139
5922
  }
6140
5923
  function resolveScanPathForExposure(raw, cwd) {
6141
- if (raw.startsWith("~/")) return resolve7(join4(osHomedir(), raw.slice(2)));
6142
- if (raw === "~") return resolve7(osHomedir());
6143
- if (isAbsolute(raw)) return resolve7(raw);
6144
- return resolve7(cwd, raw);
5924
+ if (raw.startsWith("~/")) return resolve6(join4(osHomedir(), raw.slice(2)));
5925
+ if (raw === "~") return resolve6(osHomedir());
5926
+ if (isAbsolute(raw)) return resolve6(raw);
5927
+ return resolve6(cwd, raw);
6145
5928
  }
6146
5929
  function isUnderProject(absPath, cwd) {
6147
- const projectRoot = resolve7(cwd);
5930
+ const projectRoot = resolve6(cwd);
6148
5931
  return absPath === projectRoot || absPath.startsWith(`${projectRoot}${sep}`);
6149
5932
  }
6150
5933
 
@@ -6185,7 +5968,7 @@ function ensureSidecarWritesAllowed(opts) {
6185
5968
 
6186
5969
  // kernel/sidecar/store.ts
6187
5970
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
6188
- import { dirname as dirname5, resolve as resolve8 } from "path";
5971
+ import { dirname as dirname5, resolve as resolve7 } from "path";
6189
5972
  import { createRequire as createRequire4 } from "module";
6190
5973
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
6191
5974
  import yaml2 from "js-yaml";
@@ -6293,10 +6076,10 @@ function getSidecarValidator2() {
6293
6076
  applyAjvFormats(ajv);
6294
6077
  const specRoot = resolveSpecRoot3();
6295
6078
  const annotationsSchema = JSON.parse(
6296
- readFileSync7(resolve8(specRoot, "schemas/annotations.schema.json"), "utf8")
6079
+ readFileSync7(resolve7(specRoot, "schemas/annotations.schema.json"), "utf8")
6297
6080
  );
6298
6081
  const sidecarSchema = JSON.parse(
6299
- readFileSync7(resolve8(specRoot, "schemas/sidecar.schema.json"), "utf8")
6082
+ readFileSync7(resolve7(specRoot, "schemas/sidecar.schema.json"), "utf8")
6300
6083
  );
6301
6084
  ajv.addSchema(annotationsSchema);
6302
6085
  cachedValidator = ajv.compile(sidecarSchema);
@@ -6416,11 +6199,11 @@ async function confirm(question, streams, opts) {
6416
6199
  // cli/util/git.ts
6417
6200
  import { spawnSync } from "child_process";
6418
6201
  import { existsSync as existsSync7 } from "fs";
6419
- import { dirname as dirname6, resolve as resolve9 } from "path";
6202
+ import { dirname as dirname6, resolve as resolve8 } from "path";
6420
6203
  function isInsideGitRepo(cwd) {
6421
6204
  let current = cwd;
6422
6205
  while (true) {
6423
- if (existsSync7(resolve9(current, ".git"))) return true;
6206
+ if (existsSync7(resolve8(current, ".git"))) return true;
6424
6207
  const parent = dirname6(current);
6425
6208
  if (parent === current) return false;
6426
6209
  current = parent;
@@ -6619,7 +6402,7 @@ import { existsSync as existsSync11 } from "fs";
6619
6402
 
6620
6403
  // kernel/adapters/sqlite/storage-adapter.ts
6621
6404
  import { mkdirSync as mkdirSync4 } from "fs";
6622
- import { dirname as dirname8, resolve as resolve12 } from "path";
6405
+ import { dirname as dirname8, resolve as resolve11 } from "path";
6623
6406
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
6624
6407
  import { CamelCasePlugin, Kysely, sql as sql3 } from "kysely";
6625
6408
 
@@ -7178,7 +6961,7 @@ async function migrateNodeFavorites(trx, fromPath, toPath, report) {
7178
6961
  }
7179
6962
 
7180
6963
  // kernel/adapters/sqlite/jobs.ts
7181
- import { resolve as resolve10 } from "path";
6964
+ import { resolve as resolve9 } from "path";
7182
6965
  async function pruneTerminalJobs(db, status, cutoffMs) {
7183
6966
  const rows = await db.selectFrom("state_jobs").select(["id", "filePath"]).where("status", "=", status).where("finishedAt", "is not", null).where("finishedAt", "<", cutoffMs).execute();
7184
6967
  if (rows.length === 0) {
@@ -7193,14 +6976,14 @@ async function selectReferencedJobFilePaths(db) {
7193
6976
  const rows = await db.selectFrom("state_jobs").select(["filePath"]).where("filePath", "is not", null).execute();
7194
6977
  const out = /* @__PURE__ */ new Set();
7195
6978
  for (const row of rows) {
7196
- if (row.filePath !== null) out.add(resolve10(row.filePath));
6979
+ if (row.filePath !== null) out.add(resolve9(row.filePath));
7197
6980
  }
7198
6981
  return out;
7199
6982
  }
7200
6983
 
7201
6984
  // kernel/adapters/sqlite/migrations.ts
7202
6985
  import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync } from "fs";
7203
- import { dirname as dirname7, join as join5, resolve as resolve11 } from "path";
6986
+ import { dirname as dirname7, join as join5, resolve as resolve10 } from "path";
7204
6987
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
7205
6988
  import { fileURLToPath } from "url";
7206
6989
 
@@ -7215,9 +6998,9 @@ var MIGRATIONS_TEXTS = {
7215
6998
  var FILE_RE = /^(\d{3})_([a-z0-9_]+)\.sql$/;
7216
6999
  function defaultMigrationsDir() {
7217
7000
  const here = dirname7(fileURLToPath(import.meta.url));
7218
- const flatLayout = resolve11(here, "migrations");
7001
+ const flatLayout = resolve10(here, "migrations");
7219
7002
  if (existsSync8(flatLayout)) return flatLayout;
7220
- return resolve11(here, "..", "..", "..", "migrations");
7003
+ return resolve10(here, "..", "..", "..", "migrations");
7221
7004
  }
7222
7005
  function discoverMigrations(dir = defaultMigrationsDir()) {
7223
7006
  if (!existsSync8(dir)) return [];
@@ -7287,7 +7070,7 @@ function resolveMigrationTarget(to, files) {
7287
7070
  function writePreMigrateBackup(dbPath, target) {
7288
7071
  return writeBackup(
7289
7072
  dbPath,
7290
- join5(dirname7(resolve11(dbPath)), "backups", `skill-map-pre-migrate-v${target}.db`)
7073
+ join5(dirname7(resolve10(dbPath)), "backups", `skill-map-pre-migrate-v${target}.db`)
7291
7074
  );
7292
7075
  }
7293
7076
  function applyOneMigration(db, migration) {
@@ -7322,8 +7105,8 @@ function applyOneMigration(db, migration) {
7322
7105
  }
7323
7106
  function writeBackup(dbPath, destPath) {
7324
7107
  if (dbPath === ":memory:") return null;
7325
- const absoluteSource = resolve11(dbPath);
7326
- const absoluteDest = resolve11(destPath);
7108
+ const absoluteSource = resolve10(dbPath);
7109
+ const absoluteDest = resolve10(destPath);
7327
7110
  mkdirSync3(dirname7(absoluteDest), { recursive: true });
7328
7111
  const db = new DatabaseSync2(absoluteSource);
7329
7112
  try {
@@ -8284,6 +8067,36 @@ function rowToContribution(row) {
8284
8067
  };
8285
8068
  }
8286
8069
 
8070
+ // kernel/adapters/sqlite/link-scores.ts
8071
+ async function replaceAllScanLinkScores(trx, linkScores) {
8072
+ await trx.deleteFrom("scan_link_scores").execute();
8073
+ if (linkScores.length === 0) return;
8074
+ const CHUNK = 90;
8075
+ for (let i = 0; i < linkScores.length; i += CHUNK) {
8076
+ const slice = linkScores.slice(i, i + CHUNK);
8077
+ const rows = slice.map(adjustmentToRow);
8078
+ await trx.insertInto("scan_link_scores").values(rows).execute();
8079
+ }
8080
+ }
8081
+ function adjustmentToRow(adj) {
8082
+ return {
8083
+ pluginId: adj.pluginId,
8084
+ extensionId: adj.extensionId,
8085
+ sourcePath: adj.link.source,
8086
+ target: adj.link.target,
8087
+ kind: adj.link.kind,
8088
+ normalizedTrigger: adj.link.trigger?.normalizedTrigger ?? null,
8089
+ opKind: adj.op.kind,
8090
+ opValue: adj.op.value,
8091
+ // FOLDED final confidence: by the time this writer runs, the
8092
+ // orchestrator has already applied every buffered op into
8093
+ // `link.confidence` (see `applyConfidenceAdjustments`). Denormalised
8094
+ // per row so the audit read needs no join.
8095
+ resultConfidence: adj.link.confidence,
8096
+ emittedAt: Date.now()
8097
+ };
8098
+ }
8099
+
8287
8100
  // kernel/adapters/sqlite/schema-fingerprint.ts
8288
8101
  import { createHash } from "crypto";
8289
8102
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
@@ -8370,8 +8183,18 @@ async function findNodesByTag(db, tag) {
8370
8183
  }
8371
8184
 
8372
8185
  // kernel/adapters/sqlite/scan-persistence.ts
8373
- async function persistScanResult(db, result, renameOps = [], extractorRuns = [], enrichments = [], contributions = [], registeredContributionKeys = /* @__PURE__ */ new Set(), freshlyRunTuples = /* @__PURE__ */ new Set(), contributionErrors = []) {
8186
+ async function persistScanResult(db, result, inputs = {}) {
8374
8187
  const scannedAt = validateScannedAt(result.scannedAt);
8188
+ const {
8189
+ renameOps,
8190
+ extractorRuns,
8191
+ enrichments,
8192
+ contributions,
8193
+ registeredContributionKeys,
8194
+ freshlyRunTuples,
8195
+ contributionErrors,
8196
+ linkScores
8197
+ } = resolvePersistInputs(inputs);
8375
8198
  const renames = [];
8376
8199
  await db.transaction().execute(async (trx) => {
8377
8200
  await applyRenames(trx, renameOps, renames);
@@ -8386,6 +8209,7 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
8386
8209
  freshlyRunTuples
8387
8210
  );
8388
8211
  await replaceAllScanContributionErrors(trx, contributionErrors);
8212
+ await replaceAllScanLinkScores(trx, linkScores);
8389
8213
  const tagRecords = nodesToTagRecords(result.nodes);
8390
8214
  await replaceAllScanTags(trx, tagRecords, livePathsForContrib);
8391
8215
  await upsertEnrichmentLayer(trx, result, renameOps, enrichments);
@@ -8394,6 +8218,19 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
8394
8218
  await sql2`PRAGMA wal_checkpoint(TRUNCATE)`.execute(db);
8395
8219
  return { renames };
8396
8220
  }
8221
+ function resolvePersistInputs(inputs) {
8222
+ return {
8223
+ renameOps: [],
8224
+ extractorRuns: [],
8225
+ enrichments: [],
8226
+ contributions: [],
8227
+ registeredContributionKeys: /* @__PURE__ */ new Set(),
8228
+ freshlyRunTuples: /* @__PURE__ */ new Set(),
8229
+ contributionErrors: [],
8230
+ linkScores: [],
8231
+ ...inputs
8232
+ };
8233
+ }
8397
8234
  function validateScannedAt(scannedAt) {
8398
8235
  if (!Number.isInteger(scannedAt) || scannedAt < 0) {
8399
8236
  throw new Error(
@@ -8762,7 +8599,7 @@ var SqliteStorageAdapter = class {
8762
8599
  if (this.#db) return;
8763
8600
  const path = this.#options.databasePath;
8764
8601
  if (path !== ":memory:") {
8765
- const absolute = resolve12(path);
8602
+ const absolute = resolve11(path);
8766
8603
  mkdirSync4(dirname8(absolute), { recursive: true });
8767
8604
  }
8768
8605
  if (this.#options.autoMigrate !== false) {
@@ -8899,17 +8736,7 @@ var SqliteStorageAdapter = class {
8899
8736
  };
8900
8737
  async function persistScansThroughNonTx(db, result, opts) {
8901
8738
  const defaults = applyPersistDefaults(opts);
8902
- await persistScanResult(
8903
- db,
8904
- result,
8905
- defaults.renameOps,
8906
- defaults.extractorRuns,
8907
- defaults.enrichments,
8908
- defaults.contributions,
8909
- defaults.registeredContributionKeys,
8910
- defaults.freshlyRunTuples,
8911
- defaults.contributionErrors
8912
- );
8739
+ await persistScanResult(db, result, defaults);
8913
8740
  }
8914
8741
  function applyPersistDefaults(opts) {
8915
8742
  return {
@@ -8920,6 +8747,7 @@ function applyPersistDefaults(opts) {
8920
8747
  registeredContributionKeys: /* @__PURE__ */ new Set(),
8921
8748
  freshlyRunTuples: /* @__PURE__ */ new Set(),
8922
8749
  contributionErrors: [],
8750
+ linkScores: [],
8923
8751
  ...opts
8924
8752
  };
8925
8753
  }
@@ -9076,17 +8904,7 @@ function buildTxSubset(trx) {
9076
8904
  scans: {
9077
8905
  persist: (result, opts) => {
9078
8906
  const d = applyPersistDefaults(opts);
9079
- return persistScanResult(
9080
- trx,
9081
- result,
9082
- d.renameOps,
9083
- d.extractorRuns,
9084
- d.enrichments,
9085
- d.contributions,
9086
- d.registeredContributionKeys,
9087
- d.freshlyRunTuples,
9088
- d.contributionErrors
9089
- ).then(() => void 0);
8907
+ return persistScanResult(trx, result, d).then(() => void 0);
9090
8908
  }
9091
8909
  },
9092
8910
  issues: {
@@ -9380,16 +9198,16 @@ async function tryWithSqlite(options, fn) {
9380
9198
  }
9381
9199
 
9382
9200
  // cli/commands/bump-plan.ts
9383
- import { resolve as resolve14 } from "path";
9201
+ import { resolve as resolve13 } from "path";
9384
9202
 
9385
9203
  // core/paths/path-guard.ts
9386
9204
  import { lstatSync } from "fs";
9387
- import { isAbsolute as isAbsolute2, resolve as resolve13, sep as sep2 } from "path";
9205
+ import { isAbsolute as isAbsolute2, resolve as resolve12, sep as sep2 } from "path";
9388
9206
  function assertContained(cwd, rel) {
9389
9207
  if (isAbsolute2(rel)) {
9390
9208
  throw new Error(`node path is absolute, refusing to read: ${rel}`);
9391
9209
  }
9392
- const abs = resolve13(cwd, rel);
9210
+ const abs = resolve12(cwd, rel);
9393
9211
  if (abs !== cwd && !abs.startsWith(cwd + sep2)) {
9394
9212
  throw new Error(`node path escapes repo root: ${rel}`);
9395
9213
  }
@@ -9424,7 +9242,7 @@ function planOne(node, options, invoker) {
9424
9242
  let absPath;
9425
9243
  try {
9426
9244
  assertContained(options.cwd, node.path);
9427
- absPath = resolve14(options.cwd, node.path);
9245
+ absPath = resolve13(options.cwd, node.path);
9428
9246
  } catch (err) {
9429
9247
  return {
9430
9248
  nodePath: node.path,
@@ -10047,18 +9865,18 @@ var PLUGIN_LOADER_TEXTS = {
10047
9865
  // kernel/adapters/plugin-loader/index.ts
10048
9866
  import { createRequire as createRequire5 } from "module";
10049
9867
  import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
10050
- import { join as join8, resolve as resolve17 } from "path";
9868
+ import { join as join8, resolve as resolve16 } from "path";
10051
9869
  import { pathToFileURL } from "url";
10052
9870
  import semver from "semver";
10053
9871
 
10054
9872
  // kernel/adapters/plugin-loader/id-utils.ts
10055
- import { isAbsolute as isAbsolute3, relative, resolve as resolve15 } from "path";
9873
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve14 } from "path";
10056
9874
  function fail(path, id, status, reason) {
10057
9875
  return { path, id, status, reason };
10058
9876
  }
10059
9877
  function isInsidePlugin(pluginPath, relEntry) {
10060
9878
  if (isAbsolute3(relEntry)) return false;
10061
- const abs = resolve15(pluginPath, relEntry);
9879
+ const abs = resolve14(pluginPath, relEntry);
10062
9880
  const rel = relative(pluginPath, abs);
10063
9881
  if (rel === "") return true;
10064
9882
  if (rel.startsWith("..")) return false;
@@ -10399,7 +10217,7 @@ function isDirectorySafe(path, statSync12) {
10399
10217
 
10400
10218
  // kernel/adapters/plugin-loader/storage-schemas.ts
10401
10219
  import { readFileSync as readFileSync12 } from "fs";
10402
- import { resolve as resolve16 } from "path";
10220
+ import { resolve as resolve15 } from "path";
10403
10221
  import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
10404
10222
 
10405
10223
  // kernel/adapters/plugin-store.ts
@@ -10463,7 +10281,7 @@ function compilePluginSchema(pluginPath, relPath) {
10463
10281
  errDescription: tx(PLUGIN_LOADER_TEXTS.loadErrorSchemaPathEscapesPlugin, { relPath, pluginPath })
10464
10282
  };
10465
10283
  }
10466
- const abs = resolve16(pluginPath, relPath);
10284
+ const abs = resolve15(pluginPath, relPath);
10467
10285
  let raw;
10468
10286
  try {
10469
10287
  raw = JSON.parse(readFileSync12(abs, "utf8"));
@@ -10505,7 +10323,7 @@ var PluginLoader = class {
10505
10323
  if (!entry.isDirectory()) continue;
10506
10324
  const candidate = join8(root, entry.name);
10507
10325
  if (existsSync13(join8(candidate, "plugin.json"))) {
10508
- out.push(resolve17(candidate));
10326
+ out.push(resolve16(candidate));
10509
10327
  }
10510
10328
  }
10511
10329
  }
@@ -10664,7 +10482,7 @@ var PluginLoader = class {
10664
10482
  manifest
10665
10483
  } };
10666
10484
  }
10667
- const abs = resolve17(pluginPath, relEntry);
10485
+ const abs = resolve16(pluginPath, relEntry);
10668
10486
  if (!existsSync13(abs)) {
10669
10487
  return { ok: false, failure: {
10670
10488
  ...fail(
@@ -10868,7 +10686,7 @@ function discoverExtensionEntries(pluginPath) {
10868
10686
  return out;
10869
10687
  }
10870
10688
  function collectKindEntries(pluginPath, kindDir, out) {
10871
- const kindAbs = resolve17(pluginPath, kindDir);
10689
+ const kindAbs = resolve16(pluginPath, kindDir);
10872
10690
  if (!existsSync13(kindAbs)) return;
10873
10691
  let entries;
10874
10692
  try {
@@ -10879,7 +10697,7 @@ function collectKindEntries(pluginPath, kindDir, out) {
10879
10697
  entries.sort();
10880
10698
  for (const entry of entries) {
10881
10699
  if (entry.startsWith(".")) continue;
10882
- const entryAbs = resolve17(kindAbs, entry);
10700
+ const entryAbs = resolve16(kindAbs, entry);
10883
10701
  if (!isDirectorySafe2(entryAbs)) continue;
10884
10702
  const candidate = findIndexCandidate(entryAbs);
10885
10703
  if (candidate !== null) {
@@ -10896,14 +10714,14 @@ function isDirectorySafe2(path) {
10896
10714
  }
10897
10715
  function findIndexCandidate(entryAbs) {
10898
10716
  for (const candidate of INDEX_CANDIDATES) {
10899
- if (existsSync13(resolve17(entryAbs, candidate))) return candidate;
10717
+ if (existsSync13(resolve16(entryAbs, candidate))) return candidate;
10900
10718
  }
10901
10719
  return null;
10902
10720
  }
10903
10721
  function installedSpecVersion() {
10904
10722
  const require2 = createRequire5(import.meta.url);
10905
10723
  const indexPath = require2.resolve("@skill-map/spec/index.json");
10906
- const pkgPath = resolve17(indexPath, "..", "package.json");
10724
+ const pkgPath = resolve16(indexPath, "..", "package.json");
10907
10725
  const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
10908
10726
  return pkg.version;
10909
10727
  }
@@ -11003,7 +10821,7 @@ import { join as join9, relative as relative2, sep as sep3 } from "path";
11003
10821
 
11004
10822
  // kernel/scan/ignore.ts
11005
10823
  import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
11006
- import { dirname as dirname10, resolve as resolve18 } from "path";
10824
+ import { dirname as dirname10, resolve as resolve17 } from "path";
11007
10825
  import { fileURLToPath as fileURLToPath2 } from "url";
11008
10826
  import ignoreFactory from "ignore";
11009
10827
  function buildIgnoreFilter(opts = {}) {
@@ -11032,7 +10850,7 @@ function loadBundledIgnoreText() {
11032
10850
  return loadDefaultsText();
11033
10851
  }
11034
10852
  function readIgnoreFileText(scopeRoot) {
11035
- const path = resolve18(scopeRoot, ".skillmapignore");
10853
+ const path = resolve17(scopeRoot, ".skillmapignore");
11036
10854
  if (!existsSync14(path)) return void 0;
11037
10855
  try {
11038
10856
  return readFileSync14(path, "utf8");
@@ -11061,11 +10879,11 @@ function loadDefaultsText() {
11061
10879
  function readDefaultsFromDisk() {
11062
10880
  const here = dirname10(fileURLToPath2(import.meta.url));
11063
10881
  const candidates = [
11064
- resolve18(here, "../../config/defaults/skillmapignore"),
10882
+ resolve17(here, "../../config/defaults/skillmapignore"),
11065
10883
  // src/kernel/scan/ → src/config/defaults/
11066
- resolve18(here, "../config/defaults/skillmapignore"),
10884
+ resolve17(here, "../config/defaults/skillmapignore"),
11067
10885
  // dist/cli.js → dist/config/defaults/ (siblings)
11068
- resolve18(here, "config/defaults/skillmapignore")
10886
+ resolve17(here, "config/defaults/skillmapignore")
11069
10887
  ];
11070
10888
  for (const candidate of candidates) {
11071
10889
  if (existsSync14(candidate)) {
@@ -11362,7 +11180,7 @@ function isExtensionInstance(v) {
11362
11180
  }
11363
11181
 
11364
11182
  // core/runtime/plugin-runtime/warnings.ts
11365
- import { resolve as resolve19 } from "path";
11183
+ import { resolve as resolve18 } from "path";
11366
11184
 
11367
11185
  // kernel/util/text.ts
11368
11186
  function truncateHead(s, max) {
@@ -11412,7 +11230,7 @@ function resolveRuntimeContext(opts) {
11412
11230
  return opts.runtimeContext ?? defaultRuntimeContext();
11413
11231
  }
11414
11232
  function resolveSearchPaths(opts, ctx) {
11415
- if (opts.pluginDir) return [resolve19(opts.pluginDir)];
11233
+ if (opts.pluginDir) return [resolve18(opts.pluginDir)];
11416
11234
  return [defaultProjectPluginsDir(ctx)];
11417
11235
  }
11418
11236
 
@@ -12679,7 +12497,7 @@ var CONFIG_COMMANDS = [
12679
12497
 
12680
12498
  // cli/commands/conformance.ts
12681
12499
  import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
12682
- import { dirname as dirname12, resolve as resolve22 } from "path";
12500
+ import { dirname as dirname12, resolve as resolve21 } from "path";
12683
12501
  import { fileURLToPath as fileURLToPath4 } from "url";
12684
12502
  import { Command as Command5, Option as Option5 } from "clipanion";
12685
12503
 
@@ -12687,7 +12505,7 @@ import { Command as Command5, Option as Option5 } from "clipanion";
12687
12505
  import { spawnSync as spawnSync2 } from "child_process";
12688
12506
  import { cpSync, existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync15, rmSync, statSync as statSync3 } from "fs";
12689
12507
  import { tmpdir } from "os";
12690
- import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve20 } from "path";
12508
+ import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve19 } from "path";
12691
12509
 
12692
12510
  // conformance/i18n/runner.texts.ts
12693
12511
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -12854,7 +12672,7 @@ function assertContained2(root, rel, label) {
12854
12672
  tx(CONFORMANCE_RUNNER_TEXTS.pathMustBeRelative, { label, path: rel, anchor: root })
12855
12673
  );
12856
12674
  }
12857
- const abs = resolve20(root, rel);
12675
+ const abs = resolve19(root, rel);
12858
12676
  const r = relative3(root, abs);
12859
12677
  if (r.startsWith("..") || isAbsolute5(r)) {
12860
12678
  throw new Error(
@@ -12881,7 +12699,7 @@ function evaluateAssertion(a, ctx) {
12881
12699
  } catch (err) {
12882
12700
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12883
12701
  }
12884
- const abs = resolve20(ctx.scope, a.path);
12702
+ const abs = resolve19(ctx.scope, a.path);
12885
12703
  return existsSync17(abs) ? { ok: true, type: a.type } : {
12886
12704
  ok: false,
12887
12705
  type: a.type,
@@ -12896,7 +12714,7 @@ function evaluateAssertion(a, ctx) {
12896
12714
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12897
12715
  }
12898
12716
  const fixturePath = join11(ctx.fixturesRoot, a.fixture);
12899
- const targetPath = resolve20(ctx.scope, a.path);
12717
+ const targetPath = resolve19(ctx.scope, a.path);
12900
12718
  if (!existsSync17(targetPath)) {
12901
12719
  return {
12902
12720
  ok: false,
@@ -13079,7 +12897,7 @@ var CONFORMANCE_TEXTS = {
13079
12897
 
13080
12898
  // cli/util/conformance-scopes.ts
13081
12899
  import { existsSync as existsSync18, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
13082
- import { dirname as dirname11, resolve as resolve21 } from "path";
12900
+ import { dirname as dirname11, resolve as resolve20 } from "path";
13083
12901
  import { createRequire as createRequire6 } from "module";
13084
12902
  import { fileURLToPath as fileURLToPath3 } from "url";
13085
12903
  function resolveSpecRoot4() {
@@ -13097,7 +12915,7 @@ function resolveCliWorkspaceRoot() {
13097
12915
  const here = dirname11(fileURLToPath3(import.meta.url));
13098
12916
  let cursor = here;
13099
12917
  for (let depth = 0; depth < 6; depth += 1) {
13100
- const candidate = resolve21(cursor, "plugins");
12918
+ const candidate = resolve20(cursor, "plugins");
13101
12919
  if (existsSync18(candidate) && statSync4(candidate).isDirectory()) {
13102
12920
  return cursor;
13103
12921
  }
@@ -13117,12 +12935,12 @@ function collectProviderScopes(specRoot) {
13117
12935
  } catch {
13118
12936
  return out;
13119
12937
  }
13120
- const pluginsRoot = resolve21(workspaceRoot, "plugins");
12938
+ const pluginsRoot = resolve20(workspaceRoot, "plugins");
13121
12939
  if (!existsSync18(pluginsRoot)) return out;
13122
12940
  for (const pluginEntry of readdirSync6(pluginsRoot)) {
13123
- const pluginDir = resolve21(pluginsRoot, pluginEntry);
12941
+ const pluginDir = resolve20(pluginsRoot, pluginEntry);
13124
12942
  if (!isDir(pluginDir)) continue;
13125
- const providersRoot = resolve21(pluginDir, "providers");
12943
+ const providersRoot = resolve20(pluginDir, "providers");
13126
12944
  if (!isDir(providersRoot)) continue;
13127
12945
  collectPluginProviderScopes(providersRoot, specRoot, out);
13128
12946
  }
@@ -13137,12 +12955,12 @@ function isDir(path) {
13137
12955
  }
13138
12956
  function collectPluginProviderScopes(providersRoot, specRoot, out) {
13139
12957
  for (const entry of readdirSync6(providersRoot)) {
13140
- const providerDir = resolve21(providersRoot, entry);
12958
+ const providerDir = resolve20(providersRoot, entry);
13141
12959
  if (!isDir(providerDir)) continue;
13142
- const conformanceDir = resolve21(providerDir, "conformance");
12960
+ const conformanceDir = resolve20(providerDir, "conformance");
13143
12961
  if (!existsSync18(conformanceDir)) continue;
13144
- const casesDir = resolve21(conformanceDir, "cases");
13145
- const fixturesDir = resolve21(conformanceDir, "fixtures");
12962
+ const casesDir = resolve20(conformanceDir, "cases");
12963
+ const fixturesDir = resolve20(conformanceDir, "fixtures");
13146
12964
  if (!existsSync18(casesDir) || !existsSync18(fixturesDir)) continue;
13147
12965
  out.push({
13148
12966
  id: `provider:${entry}`,
@@ -13159,8 +12977,8 @@ function specScope(specRoot) {
13159
12977
  id: "spec",
13160
12978
  kind: "spec",
13161
12979
  label: "spec",
13162
- casesDir: resolve21(specRoot, "conformance", "cases"),
13163
- fixturesDir: resolve21(specRoot, "conformance", "fixtures"),
12980
+ casesDir: resolve20(specRoot, "conformance", "cases"),
12981
+ fixturesDir: resolve20(specRoot, "conformance", "fixtures"),
13164
12982
  specRoot
13165
12983
  };
13166
12984
  }
@@ -13182,7 +13000,7 @@ function selectConformanceScopes(scope) {
13182
13000
  }
13183
13001
  function listCaseFiles(scope) {
13184
13002
  if (!existsSync18(scope.casesDir)) return [];
13185
- return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve21(scope.casesDir, entry));
13003
+ return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
13186
13004
  }
13187
13005
 
13188
13006
  // cli/commands/conformance.ts
@@ -13199,13 +13017,13 @@ function resolveBinary() {
13199
13017
  const here = dirname12(fileURLToPath4(import.meta.url));
13200
13018
  let cursor = here;
13201
13019
  for (let depth = 0; depth < 6; depth += 1) {
13202
- const candidate = resolve22(cursor, "bin", "sm.js");
13020
+ const candidate = resolve21(cursor, "bin", "sm.js");
13203
13021
  if (existsSync19(candidate)) return candidate;
13204
13022
  const parent = dirname12(cursor);
13205
13023
  if (parent === cursor) break;
13206
13024
  cursor = parent;
13207
13025
  }
13208
- return resolve22(here, "..", "..", "bin", "sm.js");
13026
+ return resolve21(here, "..", "..", "bin", "sm.js");
13209
13027
  }
13210
13028
  var ConformanceRunCommand = class extends SmCommand {
13211
13029
  static paths = [["conformance", "run"]];
@@ -13458,7 +13276,7 @@ function writeStreamSnippet(stream, header, text) {
13458
13276
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
13459
13277
 
13460
13278
  // cli/commands/db/backup.ts
13461
- import { dirname as dirname13, join as join12, resolve as resolve23 } from "path";
13279
+ import { dirname as dirname13, join as join12, resolve as resolve22 } from "path";
13462
13280
  import { Command as Command6, Option as Option6 } from "clipanion";
13463
13281
 
13464
13282
  // cli/i18n/db.texts.ts
@@ -13571,7 +13389,7 @@ var DbBackupCommand = class extends SmCommand {
13571
13389
  const exit = requireDbOrExit(path, this.context.stderr);
13572
13390
  if (exit !== null) return exit;
13573
13391
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13574
- const outPath = this.out ? resolve23(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13392
+ const outPath = this.out ? resolve22(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13575
13393
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
13576
13394
  storage.migrations.writeBackup(outPath);
13577
13395
  });
@@ -13588,7 +13406,7 @@ var DbBackupCommand = class extends SmCommand {
13588
13406
 
13589
13407
  // cli/commands/db/restore.ts
13590
13408
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
13591
- import { dirname as dirname14, resolve as resolve24 } from "path";
13409
+ import { dirname as dirname14, resolve as resolve23 } from "path";
13592
13410
  import { Command as Command7, Option as Option7 } from "clipanion";
13593
13411
 
13594
13412
  // cli/util/fs.ts
@@ -13638,7 +13456,7 @@ var DbRestoreCommand = class extends SmCommand {
13638
13456
  });
13639
13457
  async run() {
13640
13458
  const target = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13641
- const sourcePath = resolve24(this.source);
13459
+ const sourcePath = resolve23(this.source);
13642
13460
  const stderrAnsi = this.ansiFor("stderr");
13643
13461
  const sourceStat = await statOrNull(sourcePath);
13644
13462
  if (!sourceStat) {
@@ -13879,7 +13697,7 @@ var DbShellCommand = class extends SmCommand {
13879
13697
 
13880
13698
  // cli/commands/db/browser.ts
13881
13699
  import { spawn, spawnSync as spawnSync4 } from "child_process";
13882
- import { resolve as resolve25 } from "path";
13700
+ import { resolve as resolve24 } from "path";
13883
13701
  import { Command as Command10, Option as Option9 } from "clipanion";
13884
13702
  var DbBrowserCommand = class extends SmCommand {
13885
13703
  static paths = [["db", "browser"]];
@@ -13912,7 +13730,7 @@ var DbBrowserCommand = class extends SmCommand {
13912
13730
  });
13913
13731
  positional = Option9.String({ required: false });
13914
13732
  async run() {
13915
- const path = this.positional ? resolve25(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13733
+ const path = this.positional ? resolve24(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13916
13734
  if (!assertDbExists(path, this.context.stderr)) {
13917
13735
  this.printer.error(DB_TEXTS.browserRunScanFirstHint);
13918
13736
  return ExitCode.NotFound;
@@ -14830,7 +14648,7 @@ var GraphCommand = class extends SmCommand {
14830
14648
  // cli/commands/help.ts
14831
14649
  import { readFileSync as readFileSync17 } from "fs";
14832
14650
  import { createRequire as createRequire7 } from "module";
14833
- import { resolve as resolve26 } from "path";
14651
+ import { resolve as resolve25 } from "path";
14834
14652
  import { Command as Command15, Option as Option14 } from "clipanion";
14835
14653
 
14836
14654
  // cli/i18n/help.texts.ts
@@ -15144,7 +14962,7 @@ function resolveSpecVersion() {
15144
14962
  try {
15145
14963
  const req = createRequire7(import.meta.url);
15146
14964
  const indexPath = req.resolve("@skill-map/spec/index.json");
15147
- const pkgPath = resolve26(indexPath, "..", "package.json");
14965
+ const pkgPath = resolve25(indexPath, "..", "package.json");
15148
14966
  const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
15149
14967
  return pkg.version;
15150
14968
  } catch {
@@ -15452,7 +15270,7 @@ function registeredVerbPaths(cli2) {
15452
15270
 
15453
15271
  // cli/commands/hooks.ts
15454
15272
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
15455
- import { dirname as dirname16, resolve as resolve27 } from "path";
15273
+ import { dirname as dirname16, resolve as resolve26 } from "path";
15456
15274
  import { Command as Command16, Option as Option15 } from "clipanion";
15457
15275
 
15458
15276
  // cli/i18n/hooks.texts.ts
@@ -15555,8 +15373,8 @@ var HooksInstallCommand = class extends SmCommand {
15555
15373
  );
15556
15374
  return ExitCode.NotFound;
15557
15375
  }
15558
- const hooksDir = resolve27(repoRoot, ".git", "hooks");
15559
- const hookPath = resolve27(hooksDir, "pre-commit");
15376
+ const hooksDir = resolve26(repoRoot, ".git", "hooks");
15377
+ const hookPath = resolve26(hooksDir, "pre-commit");
15560
15378
  const existing = await pathExists(hookPath) ? await readFile2(hookPath, "utf8") : null;
15561
15379
  const planned2 = computePlannedHookContent(existing);
15562
15380
  if (planned2.kind === "already-installed") {
@@ -15614,7 +15432,7 @@ var HooksInstallCommand = class extends SmCommand {
15614
15432
  async function findGitRepoRoot(cwd) {
15615
15433
  let current = cwd;
15616
15434
  while (true) {
15617
- if (await pathExists(resolve27(current, ".git"))) return current;
15435
+ if (await pathExists(resolve26(current, ".git"))) return current;
15618
15436
  const parent = dirname16(current);
15619
15437
  if (parent === current) return null;
15620
15438
  current = parent;
@@ -15636,7 +15454,7 @@ var HOOKS_COMMANDS = [HooksInstallCommand];
15636
15454
 
15637
15455
  // cli/commands/init.ts
15638
15456
  import { mkdir as mkdir4, readFile as readFile3, unlink, writeFile as writeFile2 } from "fs/promises";
15639
- import { join as join17 } from "path";
15457
+ import { join as join16 } from "path";
15640
15458
  import { Command as Command17, Option as Option16 } from "clipanion";
15641
15459
 
15642
15460
  // kernel/orchestrator/index.ts
@@ -16129,8 +15947,49 @@ function runActionProjections(actions, nodes, links, emitter) {
16129
15947
  return { contributions, contributionErrors };
16130
15948
  }
16131
15949
 
15950
+ // kernel/orchestrator/confidence-score.ts
15951
+ function foldConfidence(base, ops) {
15952
+ let running = base;
15953
+ for (const op of ops) {
15954
+ if (op.kind === "set") running = op.value;
15955
+ }
15956
+ for (const op of ops) {
15957
+ if (op.kind === "delta") running += op.value;
15958
+ }
15959
+ for (const op of ops) {
15960
+ if (op.kind === "floor") running = Math.max(running, op.value);
15961
+ }
15962
+ for (const op of ops) {
15963
+ if (op.kind === "ceil") running = Math.min(running, op.value);
15964
+ }
15965
+ return clamp01(running);
15966
+ }
15967
+ function clamp01(n) {
15968
+ if (n < 0) return 0;
15969
+ if (n > 1) return 1;
15970
+ return n;
15971
+ }
15972
+ function applyConfidenceAdjustments(adjustments) {
15973
+ if (adjustments.length === 0) return;
15974
+ const byLink = /* @__PURE__ */ new Map();
15975
+ for (const adj of adjustments) {
15976
+ const bucket = byLink.get(adj.link);
15977
+ if (bucket) bucket.push(adj);
15978
+ else byLink.set(adj.link, [adj]);
15979
+ }
15980
+ for (const [link, adjs] of byLink) {
15981
+ const ops = [...adjs].sort(compareAdjustments).map((a) => a.op);
15982
+ link.confidence = foldConfidence(link.confidence, ops);
15983
+ }
15984
+ }
15985
+ function compareAdjustments(a, b) {
15986
+ if (a.pluginId !== b.pluginId) return a.pluginId < b.pluginId ? -1 : 1;
15987
+ if (a.extensionId !== b.extensionId) return a.extensionId < b.extensionId ? -1 : 1;
15988
+ return 0;
15989
+ }
15990
+
16132
15991
  // kernel/orchestrator/analyzers.ts
16133
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, signals, seedIssues = []) {
15992
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, nameCollisions, signals, seedIssues = []) {
16134
15993
  const issues = [...seedIssues];
16135
15994
  const contributions = [];
16136
15995
  const contributionErrors = [];
@@ -16141,7 +16000,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16141
16000
  expectedMdPath: o.expectedMdPath
16142
16001
  }));
16143
16002
  const scheduled = orderAnalyzersByPhase(analyzers);
16003
+ const scoreAdjustments = [];
16004
+ const scorableLinks = new Set(internalLinks);
16005
+ let scoresFolded = false;
16006
+ const foldScores = () => {
16007
+ if (scoresFolded) return;
16008
+ scoresFolded = true;
16009
+ applyConfidenceAdjustments(scoreAdjustments);
16010
+ };
16144
16011
  for (const analyzer of scheduled) {
16012
+ if (analyzer.phase !== "score") foldScores();
16145
16013
  const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
16146
16014
  const declaredContributions = readDeclaredContributionRefs(analyzer);
16147
16015
  const emitContribution = (nodePath, ref, payload) => {
@@ -16204,6 +16072,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16204
16072
  emittedAt: Date.now()
16205
16073
  });
16206
16074
  };
16075
+ const adjustConfidence = analyzer.phase === "score" ? (link, op) => {
16076
+ if (scorableLinks.has(link)) {
16077
+ scoreAdjustments.push({
16078
+ link,
16079
+ pluginId: analyzer.pluginId,
16080
+ extensionId: analyzer.id,
16081
+ op
16082
+ });
16083
+ }
16084
+ } : void 0;
16207
16085
  const emitted = await analyzer.evaluate({
16208
16086
  nodes,
16209
16087
  links: internalLinks,
@@ -16215,7 +16093,6 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16215
16093
  sidecarRoots,
16216
16094
  annotationContributions,
16217
16095
  viewContributions,
16218
- orphanJobFiles,
16219
16096
  // `issues` is the live accumulator, mutated by `issues.push(...)`
16220
16097
  // below as each analyzer's emission lands. Late-phase analyzers
16221
16098
  // (`core/issue-counter`) read it to compute cross-analyzer
@@ -16225,7 +16102,9 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16225
16102
  ...cwd ? { cwd } : {},
16226
16103
  ...reservedNodePaths ? { reservedNodePaths } : {},
16227
16104
  ...brokenLinks ? { brokenLinks } : {},
16105
+ ...nameCollisions && nameCollisions.size > 0 ? { nameCollisions } : {},
16228
16106
  ...signals && signals.length > 0 ? { signals } : {},
16107
+ ...adjustConfidence ? { adjustConfidence } : {},
16229
16108
  emitContribution
16230
16109
  });
16231
16110
  for (const issue of emitted) {
@@ -16236,13 +16115,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16236
16115
  emitter.emit(evt);
16237
16116
  await hookDispatcher.dispatch("analyzer.completed", evt);
16238
16117
  }
16239
- return { issues, contributions, contributionErrors };
16118
+ foldScores();
16119
+ return { issues, contributions, contributionErrors, linkScores: scoreAdjustments };
16240
16120
  }
16241
16121
  function orderAnalyzersByPhase(analyzers) {
16242
16122
  return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
16243
16123
  }
16244
16124
  function phaseRank(a) {
16245
- return a.phase === "aggregate" ? 1 : 0;
16125
+ if (a.phase === "score") return 0;
16126
+ if (a.phase === "aggregate") return 2;
16127
+ return 1;
16246
16128
  }
16247
16129
  function validateIssue(analyzer, issue, emitter) {
16248
16130
  const severity = issue.severity;
@@ -16293,17 +16175,6 @@ function indexPriorLinks(links, priorNodePaths, byOriginating) {
16293
16175
  else byOriginating.set(key, [link]);
16294
16176
  }
16295
16177
  }
16296
- var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
16297
- "frontmatter-invalid",
16298
- "frontmatter-malformed",
16299
- // Audit L1: parser parse-error is emitted by
16300
- // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The
16301
- // raw.parseIssues only flows through the non-cache path; a cached
16302
- // node skips the rebuild, so the prior issue MUST survive the
16303
- // incremental scan or the warning silently disappears on a clean
16304
- // re-scan of an unchanged file.
16305
- "frontmatter-parse-error"
16306
- ]);
16307
16178
  function indexPriorFrontmatterIssues(issues, byNode) {
16308
16179
  for (const issue of issues) {
16309
16180
  if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
@@ -16452,6 +16323,157 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
16452
16323
  return "obsolete";
16453
16324
  }
16454
16325
 
16326
+ // kernel/orchestrator/node-identifiers.ts
16327
+ import { posix as pathPosix4 } from "path";
16328
+ function deriveNodeIdentifiers(node, kindDescriptor) {
16329
+ const sources = kindDescriptor?.identifiers;
16330
+ if (!sources || sources.length === 0) return [];
16331
+ const out = [];
16332
+ for (const source of sources) {
16333
+ const raw = readIdentifier(source, node);
16334
+ if (!raw) continue;
16335
+ const normalised = normalizeTrigger(raw);
16336
+ if (normalised) out.push(normalised);
16337
+ }
16338
+ return out;
16339
+ }
16340
+ function readIdentifier(source, node) {
16341
+ if (source === "frontmatter.name") return readFrontmatterName(node);
16342
+ if (source === "filename-basename") return readFilenameBasename(node);
16343
+ return readDirname(node);
16344
+ }
16345
+ function readFrontmatterName(node) {
16346
+ const raw = node.frontmatter?.["name"];
16347
+ if (typeof raw !== "string") return null;
16348
+ return raw.length > 0 ? raw : null;
16349
+ }
16350
+ function readFilenameBasename(node) {
16351
+ const base = pathPosix4.basename(node.path);
16352
+ if (!base) return null;
16353
+ const ext = pathPosix4.extname(base);
16354
+ const stem = ext ? base.slice(0, -ext.length) : base;
16355
+ return stem.length > 0 ? stem : null;
16356
+ }
16357
+ function readDirname(node) {
16358
+ const dir = pathPosix4.dirname(node.path);
16359
+ if (!dir || dir === "." || dir === "/") return null;
16360
+ const base = pathPosix4.basename(dir);
16361
+ return base.length > 0 ? base : null;
16362
+ }
16363
+ function collectNameCollisions(nodes, kindRegistry) {
16364
+ const byName = indexNameClaims(nodes, kindRegistry);
16365
+ const collisions = /* @__PURE__ */ new Map();
16366
+ for (const [name, claims] of byName) {
16367
+ const distinct = dedupeClaimsByPath(claims);
16368
+ if (distinct.length >= 2) collisions.set(name, distinct);
16369
+ }
16370
+ return collisions;
16371
+ }
16372
+ function indexNameClaims(nodes, kindRegistry) {
16373
+ const byName = /* @__PURE__ */ new Map();
16374
+ for (const node of nodes) {
16375
+ const name = resolvableName(node, kindRegistry);
16376
+ if (name === null) continue;
16377
+ const bucket = byName.get(name) ?? [];
16378
+ bucket.push({ path: node.path, kind: node.kind });
16379
+ byName.set(name, bucket);
16380
+ }
16381
+ return byName;
16382
+ }
16383
+ function resolvableName(node, kindRegistry) {
16384
+ const descriptor = kindRegistry.get(`${node.provider}/${node.kind}`);
16385
+ if (!descriptor?.identifiers?.includes("frontmatter.name")) return null;
16386
+ const raw = node.frontmatter?.["name"];
16387
+ if (typeof raw !== "string" || raw.length === 0) return null;
16388
+ const normalised = normalizeTrigger(raw);
16389
+ return normalised.length > 0 ? normalised : null;
16390
+ }
16391
+ function dedupeClaimsByPath(claims) {
16392
+ return [...new Map(claims.map((c) => [c.path, c])).values()].sort(
16393
+ (a, b) => a.path.localeCompare(b.path)
16394
+ );
16395
+ }
16396
+
16397
+ // kernel/orchestrator/lift-resolved-link-confidence.ts
16398
+ function liftResolvedLinkConfidence(links, nodes, ctx) {
16399
+ if (links.length === 0) return;
16400
+ const indexes = buildIndexes(nodes, ctx);
16401
+ for (const link of links) {
16402
+ link.confidence = 1;
16403
+ applyResolution(link, indexes, ctx);
16404
+ }
16405
+ }
16406
+ function collectBrokenLinks(links, nodes, ctx) {
16407
+ const broken = /* @__PURE__ */ new Set();
16408
+ if (links.length === 0) return broken;
16409
+ const indexes = buildIndexes(nodes, ctx);
16410
+ for (const link of links) {
16411
+ if (isGenuinelyBroken(link, indexes)) broken.add(link);
16412
+ }
16413
+ return broken;
16414
+ }
16415
+ function applyResolution(link, indexes, ctx) {
16416
+ const resolution = resolve27(link, indexes, ctx);
16417
+ if (resolution === "none") return;
16418
+ link.resolvedTarget = resolution;
16419
+ }
16420
+ function buildIndexes(nodes, ctx) {
16421
+ const byPath3 = /* @__PURE__ */ new Set();
16422
+ const byName = /* @__PURE__ */ new Map();
16423
+ const nodeByPath = /* @__PURE__ */ new Map();
16424
+ for (const node of nodes) {
16425
+ byPath3.add(node.path);
16426
+ nodeByPath.set(node.path, node);
16427
+ indexNode(node, ctx, byName);
16428
+ }
16429
+ return { byPath: byPath3, byName, nodeByPath };
16430
+ }
16431
+ function resolve27(link, indexes, ctx) {
16432
+ if (indexes.byPath.has(link.target)) return link.target;
16433
+ return resolveByName(link, indexes, ctx);
16434
+ }
16435
+ function isGenuinelyBroken(link, indexes) {
16436
+ if (indexes.byPath.has(link.target)) return false;
16437
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
16438
+ if (stripped !== null && indexes.byName.has(stripped)) return false;
16439
+ return true;
16440
+ }
16441
+ function resolveByName(link, indexes, ctx) {
16442
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
16443
+ if (stripped === null) return "none";
16444
+ const candidates = indexes.byName.get(stripped);
16445
+ if (!candidates?.length) return "none";
16446
+ const allowedKinds = lookupAllowedKinds(link, indexes, ctx);
16447
+ if (!allowedKinds?.length) return "none";
16448
+ const winner = candidates.find((c) => allowedKinds.includes(c.kind));
16449
+ return winner ? winner.path : "none";
16450
+ }
16451
+ function lookupAllowedKinds(link, _indexes, ctx) {
16452
+ if (ctx.activeProvider === null) return void 0;
16453
+ return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
16454
+ }
16455
+ function stripTriggerSigil(normalized) {
16456
+ if (!normalized) return null;
16457
+ const trimmed = normalized.replace(/^[/@]/, "").trim();
16458
+ return trimmed.length === 0 ? null : trimmed;
16459
+ }
16460
+ function indexNode(node, ctx, byName) {
16461
+ const kindDescriptor = ctx.kindRegistry.get(kindKey(node));
16462
+ const normalised = deriveNodeIdentifiers(node, kindDescriptor);
16463
+ for (const name of normalised) {
16464
+ const entry = { kind: node.kind, path: node.path };
16465
+ const bucket = byName.get(name);
16466
+ if (bucket) {
16467
+ bucket.push(entry);
16468
+ } else {
16469
+ byName.set(name, [entry]);
16470
+ }
16471
+ }
16472
+ }
16473
+ function kindKey(node) {
16474
+ return `${node.provider}/${node.kind}`;
16475
+ }
16476
+
16455
16477
  // kernel/orchestrator/post-walk-transforms.ts
16456
16478
  var POST_WALK_TRANSFORMS = [
16457
16479
  {
@@ -17448,6 +17470,7 @@ async function runScanInternal(_kernel, options) {
17448
17470
  const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
17449
17471
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
17450
17472
  const brokenLinks = collectBrokenLinks(walked.internalLinks, walked.nodes, postWalkCtx);
17473
+ const nameCollisions = collectNameCollisions(walked.nodes, postWalkCtx.kindRegistry);
17451
17474
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
17452
17475
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
17453
17476
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -17462,7 +17485,6 @@ async function runScanInternal(_kernel, options) {
17462
17485
  walked.sidecarRoots,
17463
17486
  options.annotationContributions ?? [],
17464
17487
  options.viewContributions ?? [],
17465
- options.orphanJobFiles ?? [],
17466
17488
  options.referenceablePaths,
17467
17489
  options.cwd,
17468
17490
  registeredActionIds,
@@ -17470,6 +17492,7 @@ async function runScanInternal(_kernel, options) {
17470
17492
  hookDispatcher,
17471
17493
  postWalkCtx.reservedNodePaths,
17472
17494
  brokenLinks,
17495
+ nameCollisions,
17473
17496
  walked.signals,
17474
17497
  // Seed the accumulator with orchestrator-emitted frontmatter
17475
17498
  // issues so the aggregate phase (`core/issue-counter`) counts
@@ -17492,7 +17515,7 @@ async function runScanInternal(_kernel, options) {
17492
17515
  const scanCompletedEvent = makeEvent("scan.completed", { stats });
17493
17516
  emitter.emit(scanCompletedEvent);
17494
17517
  await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
17495
- return buildScanReturn(walked, issues, renameOps, stats, options, setup);
17518
+ return buildScanReturn(walked, issues, renameOps, stats, options, setup, analyzerResult.linkScores);
17496
17519
  }
17497
17520
  function buildPostWalkTransformCtx(providers, nodes, activeProvider) {
17498
17521
  const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(providers);
@@ -17620,7 +17643,7 @@ function buildScanStats(walked, issues, start) {
17620
17643
  durationMs: Date.now() - start
17621
17644
  };
17622
17645
  }
17623
- function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
17646
+ function buildScanReturn(walked, issues, renameOps, stats, options, setup, linkScores) {
17624
17647
  return {
17625
17648
  result: {
17626
17649
  schemaVersion: 1,
@@ -17642,6 +17665,7 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
17642
17665
  enrichments: walked.enrichments,
17643
17666
  contributions: walked.contributions,
17644
17667
  contributionErrors: walked.contributionErrors,
17668
+ linkScores,
17645
17669
  freshlyRunTuples: walked.freshlyRunTuples
17646
17670
  };
17647
17671
  }
@@ -17887,33 +17911,6 @@ function createKernel() {
17887
17911
  };
17888
17912
  }
17889
17913
 
17890
- // kernel/jobs/orphan-files.ts
17891
- import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
17892
- import { join as join14, resolve as resolve30 } from "path";
17893
- function findOrphanJobFiles(jobsDir, referencedPaths) {
17894
- let entries;
17895
- try {
17896
- const stat3 = statSync7(jobsDir);
17897
- if (!stat3.isDirectory()) {
17898
- return { orphanFilePaths: [], referencedCount: referencedPaths.size };
17899
- }
17900
- entries = readdirSync8(jobsDir, { withFileTypes: true });
17901
- } catch {
17902
- return { orphanFilePaths: [], referencedCount: referencedPaths.size };
17903
- }
17904
- const orphans = [];
17905
- for (const entry of entries) {
17906
- if (entry.isSymbolicLink()) continue;
17907
- if (!entry.isFile()) continue;
17908
- const name = entry.name;
17909
- if (!name.endsWith(".md")) continue;
17910
- const abs = resolve30(join14(jobsDir, name));
17911
- if (!referencedPaths.has(abs)) orphans.push(abs);
17912
- }
17913
- orphans.sort();
17914
- return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
17915
- }
17916
-
17917
17914
  // core/config/plugin-settings.ts
17918
17915
  var defaultWarn = (message) => log.warn(message);
17919
17916
  function resolveExtensionSettings(manifest, config, onWarn = defaultWarn) {
@@ -18218,9 +18215,9 @@ function resolveScanRoots(inputs) {
18218
18215
  }
18219
18216
 
18220
18217
  // core/runtime/reference-paths-walker.ts
18221
- import { readdirSync as readdirSync9, statSync as statSync8 } from "fs";
18218
+ import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
18222
18219
  import { homedir as osHomedir2 } from "os";
18223
- import { isAbsolute as isAbsolute8, join as join15, resolve as resolve31 } from "path";
18220
+ import { isAbsolute as isAbsolute8, join as join14, resolve as resolve30 } from "path";
18224
18221
  var REFERENCE_WALK_MAX_FILES = 5e4;
18225
18222
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18226
18223
  "node_modules",
@@ -18228,10 +18225,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18228
18225
  SKILL_MAP_DIR
18229
18226
  ]);
18230
18227
  function resolveScanPath(raw, cwd) {
18231
- if (raw.startsWith("~/")) return resolve31(join15(osHomedir2(), raw.slice(2)));
18232
- if (raw === "~") return resolve31(osHomedir2());
18233
- if (isAbsolute8(raw)) return resolve31(raw);
18234
- return resolve31(cwd, raw);
18228
+ if (raw.startsWith("~/")) return resolve30(join14(osHomedir2(), raw.slice(2)));
18229
+ if (raw === "~") return resolve30(osHomedir2());
18230
+ if (isAbsolute8(raw)) return resolve30(raw);
18231
+ return resolve30(cwd, raw);
18235
18232
  }
18236
18233
  function walkReferencePaths(rawRoots, cwd) {
18237
18234
  const paths = /* @__PURE__ */ new Set();
@@ -18253,14 +18250,14 @@ function walkInto(dir, out) {
18253
18250
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
18254
18251
  let entries;
18255
18252
  try {
18256
- entries = readdirSync9(dir, { withFileTypes: true });
18253
+ entries = readdirSync8(dir, { withFileTypes: true });
18257
18254
  } catch {
18258
18255
  return false;
18259
18256
  }
18260
18257
  for (const entry of entries) {
18261
18258
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
18262
18259
  if (entry.isSymbolicLink()) continue;
18263
- const full = join15(dir, entry.name);
18260
+ const full = join14(dir, entry.name);
18264
18261
  if (entry.isDirectory()) {
18265
18262
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
18266
18263
  if (walkInto(full, out)) return true;
@@ -18272,7 +18269,7 @@ function walkInto(dir, out) {
18272
18269
  }
18273
18270
  function safeStat(path) {
18274
18271
  try {
18275
- return statSync8(path);
18272
+ return statSync7(path);
18276
18273
  } catch {
18277
18274
  return null;
18278
18275
  }
@@ -18280,7 +18277,7 @@ function safeStat(path) {
18280
18277
 
18281
18278
  // core/runtime/active-provider-bootstrap.ts
18282
18279
  import { createInterface as createInterface3 } from "readline";
18283
- import { isAbsolute as isAbsolute9, join as join16 } from "path";
18280
+ import { isAbsolute as isAbsolute9, join as join15 } from "path";
18284
18281
  async function bootstrapActiveProvider(opts) {
18285
18282
  const fromCwd = resolveActiveProvider(opts.cwd, opts.providers);
18286
18283
  if (fromCwd.source === "config") {
@@ -18339,7 +18336,7 @@ function aggregateDetected(cwd, effectiveRoots, cwdDetected, providers) {
18339
18336
  out.push(id);
18340
18337
  }
18341
18338
  for (const root of effectiveRoots) {
18342
- const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
18339
+ const absRoot = isAbsolute9(root) ? root : join15(cwd, root);
18343
18340
  const r = resolveActiveProvider(absRoot, providers);
18344
18341
  for (const id of r.detected) {
18345
18342
  if (seen.has(id)) continue;
@@ -18578,7 +18575,6 @@ async function runScanForCommand(opts) {
18578
18575
  emitReferenceWalkAdvisory(walk3, opts);
18579
18576
  }
18580
18577
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
18581
- const jobsDir = defaultProjectJobsDir(ctx);
18582
18578
  const lens = await resolveActiveLens(
18583
18579
  opts,
18584
18580
  ctx,
@@ -18603,7 +18599,7 @@ async function runScanForCommand(opts) {
18603
18599
  cfg.tokenizer
18604
18600
  );
18605
18601
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
18606
- const scanned = await (willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith));
18602
+ const scanned = await (willPersist ? runPersistPath(opts, dbPath, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith));
18607
18603
  return scanned.kind === "ok" ? { ...scanned, lensAutoDetected: lens.autoDetected } : scanned;
18608
18604
  }
18609
18605
  function detectionProviders(extensions) {
@@ -18716,7 +18712,7 @@ function makePriorLoader(noBuiltIns, strict) {
18716
18712
  };
18717
18713
  }
18718
18714
  function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, recommendedNodeLimit, maxFileSizeBytes, tokenizer) {
18719
- return async (prior, priorExtractorRuns, orphanJobFiles) => {
18715
+ return async (prior, priorExtractorRuns) => {
18720
18716
  if (opts.changed && prior === null) {
18721
18717
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
18722
18718
  }
@@ -18733,14 +18729,13 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
18733
18729
  recommendedNodeLimit,
18734
18730
  maxFileSizeBytes,
18735
18731
  tokenizer,
18736
- ...priorExtractorRuns ? { priorExtractorRuns } : {},
18737
- ...orphanJobFiles ? { orphanJobFiles } : {}
18732
+ ...priorExtractorRuns ? { priorExtractorRuns } : {}
18738
18733
  });
18739
18734
  return runScanWithRenames(kernel, runOptions);
18740
18735
  };
18741
18736
  }
18742
18737
  function buildRunScanOptions(args2) {
18743
- const { opts, prior, priorExtractorRuns, orphanJobFiles, referenceablePaths } = args2;
18738
+ const { opts, prior, priorExtractorRuns, referenceablePaths } = args2;
18744
18739
  const runOptions = {
18745
18740
  roots: args2.effectiveRoots.slice(),
18746
18741
  tokenize: !opts.noTokens,
@@ -18748,11 +18743,6 @@ function buildRunScanOptions(args2) {
18748
18743
  ignoreFilter: args2.ignoreFilter,
18749
18744
  strict: args2.strict,
18750
18745
  emitter: buildRunScanEmitter(opts),
18751
- // Orphan job-file detection, empty list means "no orphans
18752
- // visible from this caller" (legacy behaviour). The orchestrator
18753
- // defaults to `[]` when the field is absent; we always pass the
18754
- // array (possibly empty) to keep the wiring uniform.
18755
- orphanJobFiles: orphanJobFiles ?? [],
18756
18746
  activeProvider: args2.activeProvider,
18757
18747
  recommendedNodeLimit: args2.recommendedNodeLimit,
18758
18748
  overrideMaxNodes: opts.maxNodes ?? null,
@@ -18795,7 +18785,7 @@ async function rebuildOnDrift(opts, dbPath) {
18795
18785
  })
18796
18786
  };
18797
18787
  }
18798
- async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) {
18788
+ async function runPersistPath(opts, dbPath, strict, loadPrior, runScanWith, extensions) {
18799
18789
  const driftError = await rebuildOnDrift(opts, dbPath);
18800
18790
  if (driftError) return driftError;
18801
18791
  let outcome;
@@ -18803,11 +18793,9 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18803
18793
  outcome = await withSqlite({ databasePath: dbPath }, async (adapter) => {
18804
18794
  const prior = await loadPrior(adapter);
18805
18795
  const priorExtractorRuns = opts.changed && prior ? await adapter.scans.loadExtractorRuns() : void 0;
18806
- const referencedJobFiles = await adapter.jobs.listReferencedFilePaths();
18807
- const orphanJobFiles = findOrphanJobFiles(jobsDir, referencedJobFiles).orphanFilePaths;
18808
18796
  let scanned;
18809
18797
  try {
18810
- scanned = await runScanWith(prior, priorExtractorRuns, orphanJobFiles);
18798
+ scanned = await runScanWith(prior, priorExtractorRuns);
18811
18799
  } catch (err) {
18812
18800
  return { kind: "scan-error", message: formatErrorMessage(err) };
18813
18801
  }
@@ -18822,6 +18810,7 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18822
18810
  enrichments: scanned.enrichments,
18823
18811
  contributions: scanned.contributions,
18824
18812
  contributionErrors: scanned.contributionErrors,
18813
+ linkScores: scanned.linkScores,
18825
18814
  registeredContributionKeys: collectRegisteredContributionKeys(extensions),
18826
18815
  freshlyRunTuples: scanned.freshlyRunTuples
18827
18816
  });
@@ -18938,7 +18927,7 @@ var InitCommand = class extends SmCommand {
18938
18927
  async run() {
18939
18928
  const ctx = defaultRuntimeContext();
18940
18929
  const scopeRoot = ctx.cwd;
18941
- const skillMapDir = join17(scopeRoot, SKILL_MAP_DIR);
18930
+ const skillMapDir = join16(scopeRoot, SKILL_MAP_DIR);
18942
18931
  const settingsPath = defaultSettingsPath(scopeRoot);
18943
18932
  const localPath = defaultLocalSettingsPath(scopeRoot);
18944
18933
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -18984,7 +18973,7 @@ var InitCommand = class extends SmCommand {
18984
18973
  const okGlyph = ansi.green("\u2713");
18985
18974
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
18986
18975
  if (updated) {
18987
- const gitignorePath = join17(scopeRoot, ".gitignore");
18976
+ const gitignorePath = join16(scopeRoot, ".gitignore");
18988
18977
  printer.info(
18989
18978
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
18990
18979
  glyph: okGlyph,
@@ -19045,7 +19034,7 @@ async function safeUnlink(path) {
19045
19034
  }
19046
19035
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
19047
19036
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
19048
- const gitignorePath = join17(scopeRoot, ".gitignore");
19037
+ const gitignorePath = join16(scopeRoot, ".gitignore");
19049
19038
  if (wouldAdd.length === 0) {
19050
19039
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
19051
19040
  } else if (wouldAdd.length === 1) {
@@ -19150,7 +19139,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, stdin, ansi) {
19150
19139
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
19151
19140
  }
19152
19141
  async function previewGitignoreEntries(scopeRoot, entries) {
19153
- const path = join17(scopeRoot, ".gitignore");
19142
+ const path = join16(scopeRoot, ".gitignore");
19154
19143
  const body = await pathExists(path) ? await readFile3(path, "utf8") : "";
19155
19144
  const present = new Set(
19156
19145
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -19158,7 +19147,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
19158
19147
  return entries.filter((entry) => !present.has(entry));
19159
19148
  }
19160
19149
  async function ensureGitignoreEntries(scopeRoot, entries) {
19161
- const path = join17(scopeRoot, ".gitignore");
19150
+ const path = join16(scopeRoot, ".gitignore");
19162
19151
  let body = "";
19163
19152
  if (await pathExists(path)) {
19164
19153
  body = await readFile3(path, "utf8");
@@ -19699,6 +19688,33 @@ import { unlink as unlink2 } from "fs/promises";
19699
19688
  import { relative as relative6 } from "path";
19700
19689
  import { Command as Command19, Option as Option18 } from "clipanion";
19701
19690
 
19691
+ // kernel/jobs/orphan-files.ts
19692
+ import { readdirSync as readdirSync9, statSync as statSync8 } from "fs";
19693
+ import { join as join17, resolve as resolve31 } from "path";
19694
+ function findOrphanJobFiles(jobsDir, referencedPaths) {
19695
+ let entries;
19696
+ try {
19697
+ const stat3 = statSync8(jobsDir);
19698
+ if (!stat3.isDirectory()) {
19699
+ return { orphanFilePaths: [], referencedCount: referencedPaths.size };
19700
+ }
19701
+ entries = readdirSync9(jobsDir, { withFileTypes: true });
19702
+ } catch {
19703
+ return { orphanFilePaths: [], referencedCount: referencedPaths.size };
19704
+ }
19705
+ const orphans = [];
19706
+ for (const entry of entries) {
19707
+ if (entry.isSymbolicLink()) continue;
19708
+ if (!entry.isFile()) continue;
19709
+ const name = entry.name;
19710
+ if (!name.endsWith(".md")) continue;
19711
+ const abs = resolve31(join17(jobsDir, name));
19712
+ if (!referencedPaths.has(abs)) orphans.push(abs);
19713
+ }
19714
+ orphans.sort();
19715
+ return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
19716
+ }
19717
+
19702
19718
  // cli/i18n/jobs.texts.ts
19703
19719
  var JOBS_TEXTS = {
19704
19720
  pruneErrorPrefix: "{{glyph}} sm job prune: {{message}}\n",
@@ -23899,6 +23915,7 @@ function createWatcherRuntime(opts) {
23899
23915
  enrichments,
23900
23916
  contributions,
23901
23917
  contributionErrors,
23918
+ linkScores,
23902
23919
  freshlyRunTuples
23903
23920
  } = ran;
23904
23921
  await withSqlite(
@@ -23909,6 +23926,7 @@ function createWatcherRuntime(opts) {
23909
23926
  enrichments,
23910
23927
  contributions,
23911
23928
  contributionErrors,
23929
+ linkScores,
23912
23930
  registeredContributionKeys: collectRegisteredContributionKeys(composed),
23913
23931
  freshlyRunTuples
23914
23932
  })
@@ -24391,7 +24409,7 @@ var ScanCommand = class extends SmCommand {
24391
24409
  details: `
24392
24410
  Walks the given roots with the built-in claude Provider, runs the
24393
24411
  frontmatter / slash / at-directive / external-url-counter
24394
- extractors per node, then the trigger-collision / broken-ref /
24412
+ extractors per node, then the name-collision / broken-ref /
24395
24413
  superseded analyzers over the full graph. Emits a ScanResult
24396
24414
  conforming to scan-result.schema.json.
24397
24415
 
@@ -28494,6 +28512,43 @@ var WsBroadcaster = class {
28494
28512
  }
28495
28513
  };
28496
28514
 
28515
+ // server/heartbeat.ts
28516
+ var WS_HEARTBEAT_INTERVAL_MS = 3e4;
28517
+ function startWsHeartbeat(wss, opts = {}) {
28518
+ const intervalMs = opts.intervalMs ?? WS_HEARTBEAT_INTERVAL_MS;
28519
+ const alive = /* @__PURE__ */ new WeakMap();
28520
+ const onConnection = (socket) => {
28521
+ alive.set(socket, true);
28522
+ socket.on("pong", () => {
28523
+ alive.set(socket, true);
28524
+ });
28525
+ };
28526
+ wss.on("connection", onConnection);
28527
+ const timer = setInterval(() => {
28528
+ for (const socket of wss.clients) {
28529
+ if (alive.get(socket) === false) {
28530
+ socket.terminate();
28531
+ continue;
28532
+ }
28533
+ alive.set(socket, false);
28534
+ try {
28535
+ socket.ping();
28536
+ } catch {
28537
+ }
28538
+ }
28539
+ }, intervalMs);
28540
+ timer.unref?.();
28541
+ let stopped = false;
28542
+ return {
28543
+ stop() {
28544
+ if (stopped) return;
28545
+ stopped = true;
28546
+ clearInterval(timer);
28547
+ wss.off("connection", onConnection);
28548
+ }
28549
+ };
28550
+ }
28551
+
28497
28552
  // server/kind-registry.ts
28498
28553
  function buildKindRegistry(providers) {
28499
28554
  const registry = {};
@@ -28777,6 +28832,7 @@ async function createServer(options, extra = {}) {
28777
28832
  });
28778
28833
  const wss = new WebSocketServer({ noServer: true });
28779
28834
  const server = await listenAsync(app.fetch, wss, options.host, options.port);
28835
+ const heartbeat = startWsHeartbeat(wss);
28780
28836
  const addr = server.address();
28781
28837
  const address = normalizeAddress(addr, options.host, options.port);
28782
28838
  let watcherService = null;
@@ -28810,6 +28866,7 @@ async function createServer(options, extra = {}) {
28810
28866
  const close = async () => {
28811
28867
  if (closed) return;
28812
28868
  closed = true;
28869
+ heartbeat.stop();
28813
28870
  if (watcherService) {
28814
28871
  try {
28815
28872
  await watcherService.stop();
@@ -31152,4 +31209,4 @@ function resolveBareDefault() {
31152
31209
  process.exit(ExitCode.Error);
31153
31210
  }
31154
31211
  //# sourceMappingURL=cli.js.map
31155
- //# debugId=49fdc2af-2694-5cd0-ac19-33f8d1dffa9e
31212
+ //# debugId=e6038423-ea97-5e66-b371-0916d382d819