@rhize/skill-forge 0.13.0 → 0.14.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/README.md +2 -0
- package/dist/cli.js +37 -8
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -174,6 +174,8 @@ target skills root until you decide.
|
|
|
174
174
|
| `--ingest` | Pro (free during the 0.x beta). After a successful promote, hands off to a coding agent — see [Ingestion handoff](#ingestion-handoff---ingest). |
|
|
175
175
|
| `--skill-map <path>` | Path to a generated `rhize-plugins` skill map (Phase 4 of that repo's skill-map-graph-substrate plan). When given, ranks the candidate's name/description against every `skill` node in the map — a near-duplicate of an already-shipped marketplace skill is folded into the safety findings as a `HIGH`-severity finding (escalates the verdict to `block`, so it's held rather than silently promoted); a moderate overlap is `MEDIUM` (`warn`). Free (not Pro-gated) — this enforces the marketplace's own curation rule ("close the gap, don't duplicate"), not a premium ranking feature. Missing/unreadable map: printed to stderr, never fatal. **Two map variants exist, with different coverage**: the **static** map (`rhize-plugins/generated/skill-map.static.json`) is first-party-only — it only knows about that marketplace's own skills, so it cannot catch a candidate that duplicates an *installed third-party plugin's* skill. The **resolved** map (`~/.claude/context-manager/skill-map.resolved.json`, produced by `rhize-context-manager`) additionally carries the full third-party ecosystem inventory. Point `--skill-map` at the resolved map when you want ecosystem-wide duplicate detection; the static map is enough only when you're curating a single first-party marketplace against itself. |
|
|
176
176
|
|
|
177
|
+
**Observability (v0.14.0):** the `--skill-map` curation check now always reports that it ran. A below-threshold PASS previously left no trace at all — no line in the report, no field in `--json` — making "checked 45 skills, nothing crossed the threshold" indistinguishable from "the check never ran". That ambiguity caused a correct PASS to be written up as a broken gate. The report now carries a `Marketplace overlap` line (`checked N skills — closest "x" (score), none over threshold` / `skipped — <reason>`) and `--json` a `mapOverlap` object with `status`, `compared`, `topMatch`, and `matches`. `topMatch` is informational only: it is never graded and never produces a finding. **Scoring and both thresholds are unchanged.** Note the check is lexical, not semantic — two skills can claim the same invocation trigger in different words and score far below threshold (measured: 0.134), so competing `description:` triggers still need a human read. See `gate/mapOverlap.ts`'s module doc.
|
|
178
|
+
|
|
177
179
|
**Bugfix (v0.13.0):** `--skill-map` previously matched nothing against either real map variant — the loader read a `type` field that the real generated artifact never sets (it uses `kind`), so every node was silently invisible to the overlap check. `loadSkillMap()` now normalizes `kind` → `type` at load time; regression coverage lives in `test/mapOverlap.realmap.test.ts` against vendored real-map snapshots. If you were relying on `--skill-map` before v0.13.0, it was a no-op — re-run `add`/`scan` on anything you're unsure about.
|
|
178
180
|
|
|
179
181
|
**Extends-declared overlap exemption**: a candidate can declare `metadata.rhize.extends:
|
package/dist/cli.js
CHANGED
|
@@ -3505,18 +3505,23 @@ function nodeText(map, node) {
|
|
|
3505
3505
|
}
|
|
3506
3506
|
function scanMapOverlap(candidate, map) {
|
|
3507
3507
|
const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
|
|
3508
|
-
if (candidateTokens.size === 0) return { matches: [] };
|
|
3508
|
+
if (candidateTokens.size === 0) return { matches: [], compared: 0 };
|
|
3509
3509
|
const matches = [];
|
|
3510
|
+
let compared = 0;
|
|
3511
|
+
let topMatch;
|
|
3510
3512
|
for (const node of map.nodes) {
|
|
3511
3513
|
if (node.type !== "skill") continue;
|
|
3514
|
+
compared += 1;
|
|
3512
3515
|
const otherTokens = tokenize2(nodeText(map, node));
|
|
3513
3516
|
const s = score3(candidateTokens, otherTokens);
|
|
3517
|
+
const match = { skillId: node.id, name: node.name ?? node.id, score: s };
|
|
3518
|
+
if (!topMatch || s > topMatch.score) topMatch = match;
|
|
3514
3519
|
if (s >= REPORT_THRESHOLD) {
|
|
3515
|
-
matches.push(
|
|
3520
|
+
matches.push(match);
|
|
3516
3521
|
}
|
|
3517
3522
|
}
|
|
3518
3523
|
matches.sort((a, b) => b.score - a.score);
|
|
3519
|
-
return { matches };
|
|
3524
|
+
return { matches, compared, topMatch };
|
|
3520
3525
|
}
|
|
3521
3526
|
function mapOverlapFinding(result) {
|
|
3522
3527
|
const top = result.matches[0];
|
|
@@ -3608,6 +3613,17 @@ function renderCapabilitiesLine(profile) {
|
|
|
3608
3613
|
);
|
|
3609
3614
|
return `${padLabel("Capabilities")}: ${counts} (declaredConfidence: ${declaredConfidence})`;
|
|
3610
3615
|
}
|
|
3616
|
+
function renderMapOverlapLines(report) {
|
|
3617
|
+
if (!report || report.status === "not-requested") return [];
|
|
3618
|
+
if (report.status === "skipped") {
|
|
3619
|
+
return [`${padLabel("Marketplace overlap")}: skipped \u2014 ${report.reason ?? "map unavailable"}`];
|
|
3620
|
+
}
|
|
3621
|
+
const top = report.topMatch;
|
|
3622
|
+
const flagged = report.matches?.length ?? 0;
|
|
3623
|
+
const closest = top ? `closest "${top.name}" (${top.score})` : "no comparable skills";
|
|
3624
|
+
const verdict = flagged > 0 ? `${flagged} at/over threshold` : "none over threshold";
|
|
3625
|
+
return [`${padLabel("Marketplace overlap")}: checked ${report.compared ?? 0} skills \u2014 ${closest}, ${verdict}`];
|
|
3626
|
+
}
|
|
3611
3627
|
function renderReport(result) {
|
|
3612
3628
|
const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
|
|
3613
3629
|
const { name, version, license } = profileHeaderFields(profile);
|
|
@@ -3622,6 +3638,7 @@ function renderReport(result) {
|
|
|
3622
3638
|
`${padLabel("Artifact type")}: ${profile.artifactType}`,
|
|
3623
3639
|
...profile.kind === "mcp" ? [renderCapabilitiesLine(profile)] : [],
|
|
3624
3640
|
...overlapLines,
|
|
3641
|
+
...renderMapOverlapLines(result.mapOverlap),
|
|
3625
3642
|
renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
|
|
3626
3643
|
renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
|
|
3627
3644
|
`${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
|
|
@@ -3676,13 +3693,23 @@ function tryMcpOverlap(profile, mcpTargets) {
|
|
|
3676
3693
|
}
|
|
3677
3694
|
}
|
|
3678
3695
|
function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
|
|
3679
|
-
if (!skillMapPath) return
|
|
3696
|
+
if (!skillMapPath) return { report: { status: "not-requested" } };
|
|
3680
3697
|
try {
|
|
3681
3698
|
const map = loadSkillMap(skillMapPath);
|
|
3682
|
-
|
|
3699
|
+
const result = scanMapOverlap(candidate, map);
|
|
3700
|
+
return {
|
|
3701
|
+
decision: mapOverlapDecision(result, extendsDeclarations),
|
|
3702
|
+
report: {
|
|
3703
|
+
status: "checked",
|
|
3704
|
+
compared: result.compared,
|
|
3705
|
+
topMatch: result.topMatch,
|
|
3706
|
+
matches: result.matches
|
|
3707
|
+
}
|
|
3708
|
+
};
|
|
3683
3709
|
} catch (err) {
|
|
3684
|
-
|
|
3685
|
-
|
|
3710
|
+
const reason = err.message;
|
|
3711
|
+
console.error(`[skill-forge] skill-map overlap check skipped: ${reason}`);
|
|
3712
|
+
return { report: { status: "skipped", reason } };
|
|
3686
3713
|
}
|
|
3687
3714
|
}
|
|
3688
3715
|
function resolveMcpCandidatePath(entry) {
|
|
@@ -3705,7 +3732,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3705
3732
|
const proStatus = proFeatureStatus(proFeatureLabel);
|
|
3706
3733
|
const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
|
|
3707
3734
|
const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
|
|
3708
|
-
const
|
|
3735
|
+
const mapOutcome = artifact === "mcp" ? { decision: void 0, report: { status: "not-requested" } } : tryMapOverlapDecision(
|
|
3709
3736
|
skillMapPath,
|
|
3710
3737
|
{
|
|
3711
3738
|
name: "name" in profile ? profile.name : void 0,
|
|
@@ -3713,6 +3740,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3713
3740
|
},
|
|
3714
3741
|
profile.kind === "skill" ? profile.extends : []
|
|
3715
3742
|
);
|
|
3743
|
+
const mapDecision = mapOutcome.decision;
|
|
3716
3744
|
const mapFinding = mapDecision?.finding;
|
|
3717
3745
|
const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
|
|
3718
3746
|
const advisory = formatAuditAdvisory(readAuditState(), {
|
|
@@ -3728,6 +3756,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3728
3756
|
overlap,
|
|
3729
3757
|
overlapLocked: !proStatus.enabled,
|
|
3730
3758
|
notices: notices.length > 0 ? notices : void 0,
|
|
3759
|
+
mapOverlap: mapOutcome.report,
|
|
3731
3760
|
safety: {
|
|
3732
3761
|
verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
|
|
3733
3762
|
findings: mapSafetyFindings(combinedSafety.findings)
|