@iceinvein/agent-skills 0.1.30 → 0.1.32
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/package.json +1 -1
- package/skills/index.json +1 -1
- package/skills/magpie/SKILL.md +24 -1
- package/skills/magpie/bun.lock +91 -0
- package/skills/magpie/package.json +3 -0
- package/skills/magpie/scripts/__tests__/highlight.test.ts +91 -0
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +55 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +129 -10
- package/skills/magpie/scripts/__tests__/render-diff.test.ts +83 -1
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +24 -8
- package/skills/magpie/scripts/__tests__/render-issues-list.test.ts +38 -6
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +3 -1
- package/skills/magpie/scripts/__tests__/types.test.ts +227 -14
- package/skills/magpie/scripts/highlight.ts +113 -0
- package/skills/magpie/scripts/post-cmd.ts +61 -6
- package/skills/magpie/scripts/render-annotation.ts +27 -5
- package/skills/magpie/scripts/render-diff.ts +82 -19
- package/skills/magpie/scripts/render-findings.ts +18 -4
- package/skills/magpie/scripts/render-issues-list.ts +4 -1
- package/skills/magpie/scripts/types.ts +219 -22
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +43 -2
package/package.json
CHANGED
package/skills/index.json
CHANGED
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"name": "magpie",
|
|
198
198
|
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec, and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request.",
|
|
199
199
|
"type": "prompt",
|
|
200
|
-
"version": "0.
|
|
200
|
+
"version": "0.5.0"
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
"name": "module-secret-auditor",
|
package/skills/magpie/SKILL.md
CHANGED
|
@@ -83,13 +83,29 @@ Write findings to $RUN_DIR/findings/<focus>.json before returning. The file MUST
|
|
|
83
83
|
"title": string, // one line
|
|
84
84
|
"description": string, // 2-4 short labelled paragraphs (see below). Cite code with file:line.
|
|
85
85
|
"suggestion": { // OPTIONAL; omit the key entirely if not applicable. NOT "recommendation"
|
|
86
|
-
"body": string,
|
|
86
|
+
"body": string, // LITERAL replacement source code for lines startLine..endLine. NOT prose. See rules below.
|
|
87
87
|
"startLine": number,
|
|
88
88
|
"endLine": number
|
|
89
89
|
},
|
|
90
90
|
"domain": "<focus>" // literal focus id, copied verbatim
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
**Enum values are exact strings, not free-form prose.** Every value above between `"..."` and `|` markers is a literal token. Copy them verbatim. Specifically:
|
|
94
|
+
|
|
95
|
+
- `severity`, `risk.impact`, `risk.confidence` use category names (e.g. `high`, `low`), not sentences.
|
|
96
|
+
- `risk.likelihood` describes frequency, not impact. Valid values are exactly `likely`, `possible`, `edge-case`, `unknown`. NEVER use `high`/`medium`/`low` here (those are likelihood-as-impact and will be auto-corrected, but pick the right axis).
|
|
97
|
+
- `risk.action` is the disposition tag, not the recommendation text. Valid values are exactly `must-fix`, `should-fix`, `consider`, `optional`. The recommendation prose belongs in `description` under `Suggested direction:`, never in `risk.action`.
|
|
98
|
+
|
|
99
|
+
Bad (will be silently coerced, do not rely on this):
|
|
100
|
+
```
|
|
101
|
+
"risk": { "impact": "blocker", "likelihood": "high", "confidence": "very high", "action": "Fix this immediately before merging." }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Good:
|
|
105
|
+
```
|
|
106
|
+
"risk": { "impact": "critical", "likelihood": "likely", "confidence": "high", "action": "must-fix" }
|
|
107
|
+
```
|
|
108
|
+
|
|
93
109
|
`description` MUST be a sequence of short labelled paragraphs separated by blank lines, using these exact prefixes when they apply:
|
|
94
110
|
|
|
95
111
|
- `Observation: <one idea, what the diff actually does and where>`
|
|
@@ -99,6 +115,13 @@ Write findings to $RUN_DIR/findings/<focus>.json before returning. The file MUST
|
|
|
99
115
|
|
|
100
116
|
One idea per paragraph. Do not collapse them into a single wall of text. Do not invent extra labels. If a section doesn't apply, omit it. The interactive report and the GitHub comment both parse these labels and render them as section headers, so missing labels degrade the output.
|
|
101
117
|
|
|
118
|
+
**`suggestion.body` rules.** When present, `body` MUST be the literal source code that should replace lines `startLine..endLine` verbatim. It is fenced as `` ```suggestion `` on GitHub and rendered as a one-click "Apply" button; the bytes you write here get committed as-is to the PR. Therefore:
|
|
119
|
+
|
|
120
|
+
- Write code only. No leading "Strip the delimiter...", "Add a check that...", or other prose. The prose explanation belongs in `description` under `Suggested direction:`.
|
|
121
|
+
- Match the file's existing indentation and language exactly. Include only the lines being replaced; do not include unchanged surrounding context.
|
|
122
|
+
- If you cannot produce an exact, copy-pasteable replacement (you don't know the surrounding code, the fix spans multiple files, or the change is conceptual), OMIT the `suggestion` key entirely. A prose `Suggested direction:` in `description` is the right channel for that.
|
|
123
|
+
- Wrapping the code in a `` ``` `` fence inside `body` is tolerated (the poster hoists the inner code out), but bare code is preferred.
|
|
124
|
+
|
|
102
125
|
If you have no findings, write []. Return as your final tool result a single line: `<focus>: <N> findings (<blocker>/<high>/<medium>/<low>)`. Do not include other prose.
|
|
103
126
|
```
|
|
104
127
|
|
package/skills/magpie/bun.lock
CHANGED
|
@@ -4,18 +4,109 @@
|
|
|
4
4
|
"workspaces": {
|
|
5
5
|
"": {
|
|
6
6
|
"name": "magpie",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"shiki": "^1.24.0",
|
|
9
|
+
},
|
|
7
10
|
"devDependencies": {
|
|
8
11
|
"@types/bun": "^1.2.0",
|
|
9
12
|
},
|
|
10
13
|
},
|
|
11
14
|
},
|
|
12
15
|
"packages": {
|
|
16
|
+
"@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
|
|
17
|
+
|
|
18
|
+
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
|
|
19
|
+
|
|
20
|
+
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="],
|
|
21
|
+
|
|
22
|
+
"@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="],
|
|
23
|
+
|
|
24
|
+
"@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="],
|
|
25
|
+
|
|
26
|
+
"@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
|
27
|
+
|
|
28
|
+
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
|
29
|
+
|
|
13
30
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
|
14
31
|
|
|
32
|
+
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
|
33
|
+
|
|
34
|
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
|
35
|
+
|
|
15
36
|
"@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="],
|
|
16
37
|
|
|
38
|
+
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
|
39
|
+
|
|
40
|
+
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
|
|
41
|
+
|
|
17
42
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
|
18
43
|
|
|
44
|
+
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
|
45
|
+
|
|
46
|
+
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
|
47
|
+
|
|
48
|
+
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
|
49
|
+
|
|
50
|
+
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
|
51
|
+
|
|
52
|
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
|
53
|
+
|
|
54
|
+
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
|
55
|
+
|
|
56
|
+
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
|
57
|
+
|
|
58
|
+
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
|
59
|
+
|
|
60
|
+
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
|
61
|
+
|
|
62
|
+
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
|
63
|
+
|
|
64
|
+
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
|
65
|
+
|
|
66
|
+
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
|
67
|
+
|
|
68
|
+
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
|
69
|
+
|
|
70
|
+
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
|
71
|
+
|
|
72
|
+
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
|
73
|
+
|
|
74
|
+
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
|
75
|
+
|
|
76
|
+
"oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
|
77
|
+
|
|
78
|
+
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
|
79
|
+
|
|
80
|
+
"regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
|
81
|
+
|
|
82
|
+
"regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
|
83
|
+
|
|
84
|
+
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
|
85
|
+
|
|
86
|
+
"shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
|
|
87
|
+
|
|
88
|
+
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
|
89
|
+
|
|
90
|
+
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
|
91
|
+
|
|
92
|
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
|
93
|
+
|
|
19
94
|
"undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="],
|
|
95
|
+
|
|
96
|
+
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
|
97
|
+
|
|
98
|
+
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
|
99
|
+
|
|
100
|
+
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
|
101
|
+
|
|
102
|
+
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
|
103
|
+
|
|
104
|
+
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
|
105
|
+
|
|
106
|
+
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
|
107
|
+
|
|
108
|
+
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
|
109
|
+
|
|
110
|
+
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
|
20
111
|
}
|
|
21
112
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
getHighlighter,
|
|
4
|
+
highlightCodeBlock,
|
|
5
|
+
highlightDiffSide,
|
|
6
|
+
languageFromPath,
|
|
7
|
+
} from '../highlight.ts'
|
|
8
|
+
|
|
9
|
+
describe('languageFromPath', () => {
|
|
10
|
+
test('maps known extensions to languages', () => {
|
|
11
|
+
expect(languageFromPath('src/foo.ts')).toBe('typescript')
|
|
12
|
+
expect(languageFromPath('src/foo.tsx')).toBe('tsx')
|
|
13
|
+
expect(languageFromPath('a/b/c.py')).toBe('python')
|
|
14
|
+
expect(languageFromPath('main.go')).toBe('go')
|
|
15
|
+
expect(languageFromPath('lib.rs')).toBe('rust')
|
|
16
|
+
expect(languageFromPath('App.java')).toBe('java')
|
|
17
|
+
expect(languageFromPath('app.rb')).toBe('ruby')
|
|
18
|
+
expect(languageFromPath('run.sh')).toBe('bash')
|
|
19
|
+
expect(languageFromPath('data.json')).toBe('json')
|
|
20
|
+
expect(languageFromPath('ci.yml')).toBe('yaml')
|
|
21
|
+
expect(languageFromPath('readme.md')).toBe('markdown')
|
|
22
|
+
expect(languageFromPath('page.html')).toBe('html')
|
|
23
|
+
expect(languageFromPath('styles.css')).toBe('css')
|
|
24
|
+
expect(languageFromPath('schema.sql')).toBe('sql')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('falls back to plaintext for unknown/empty', () => {
|
|
28
|
+
expect(languageFromPath('LICENSE')).toBe('plaintext')
|
|
29
|
+
expect(languageFromPath('file.xyz')).toBe('plaintext')
|
|
30
|
+
expect(languageFromPath('')).toBe('plaintext')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('is case-insensitive on extension', () => {
|
|
34
|
+
expect(languageFromPath('FOO.TS')).toBe('typescript')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('highlightCodeBlock', () => {
|
|
39
|
+
test('wraps output in shiki pre/code', async () => {
|
|
40
|
+
const hl = await getHighlighter()
|
|
41
|
+
const html = highlightCodeBlock(hl, 'const x = 1', 'typescript')
|
|
42
|
+
expect(html).toContain('<pre')
|
|
43
|
+
expect(html).toContain('class="shiki')
|
|
44
|
+
expect(html).toContain('<code>')
|
|
45
|
+
expect(html).toContain('<span class="line">')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('falls back to plaintext for unregistered langs without throwing', async () => {
|
|
49
|
+
const hl = await getHighlighter()
|
|
50
|
+
const html = highlightCodeBlock(hl, 'hello world', 'klingon')
|
|
51
|
+
expect(html).toContain('<pre')
|
|
52
|
+
expect(html).toContain('hello world')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('emits dual-theme CSS variables for prefers-color-scheme', async () => {
|
|
56
|
+
const hl = await getHighlighter()
|
|
57
|
+
const html = highlightCodeBlock(hl, 'const x = 1', 'typescript')
|
|
58
|
+
expect(html).toMatch(/--shiki-dark/)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('highlightDiffSide', () => {
|
|
63
|
+
test('returns one entry per source line', async () => {
|
|
64
|
+
const hl = await getHighlighter()
|
|
65
|
+
const code = 'const a = 1\nconst b = 2\nconst c = 3'
|
|
66
|
+
const lines = highlightDiffSide(hl, code, 'typescript')
|
|
67
|
+
expect(lines.length).toBe(3)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('handles multi-line template literal without losing lines', async () => {
|
|
71
|
+
const hl = await getHighlighter()
|
|
72
|
+
const code = 'const x = `line1\nline2\nline3`\nconst y = 2'
|
|
73
|
+
const lines = highlightDiffSide(hl, code, 'typescript')
|
|
74
|
+
expect(lines.length).toBe(4)
|
|
75
|
+
expect(lines[0]).toContain('line1')
|
|
76
|
+
expect(lines[1]).toContain('line2')
|
|
77
|
+
expect(lines[2]).toContain('line3')
|
|
78
|
+
expect(lines[3]).toContain('y')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('returns empty array for empty input', async () => {
|
|
82
|
+
const hl = await getHighlighter()
|
|
83
|
+
expect(highlightDiffSide(hl, '', 'typescript')).toEqual([])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('plaintext fallback still splits per line', async () => {
|
|
87
|
+
const hl = await getHighlighter()
|
|
88
|
+
const lines = highlightDiffSide(hl, 'a\nb\nc', 'plaintext')
|
|
89
|
+
expect(lines.length).toBe(3)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -109,6 +109,61 @@ test('formatPostBody omits the suggestion block when no suggestion is set', () =
|
|
|
109
109
|
expect(body).not.toContain('```suggestion')
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
+
test('formatInlineBody extracts an embedded fenced code block out of the suggestion body', () => {
|
|
113
|
+
const body = formatInlineBody({
|
|
114
|
+
...findingA,
|
|
115
|
+
line: 12,
|
|
116
|
+
suggestion: {
|
|
117
|
+
body: 'Switch to the inference profile and widen IAM:\n\n```hcl\nstatement {\n effect = "Allow"\n}\n```\n\nAt minimum, log the failure.',
|
|
118
|
+
startLine: 12,
|
|
119
|
+
endLine: 12,
|
|
120
|
+
},
|
|
121
|
+
} as never)
|
|
122
|
+
// Prose appears as ordinary markdown, not inside the suggestion fence.
|
|
123
|
+
expect(body).toContain('Switch to the inference profile and widen IAM:')
|
|
124
|
+
expect(body).toContain('At minimum, log the failure.')
|
|
125
|
+
// The suggestion body contains only the extracted code, not the prose.
|
|
126
|
+
expect(body).toContain('```suggestion\nstatement {\n effect = "Allow"\n}\n```')
|
|
127
|
+
// The hidden finding marker stays inside an HTML comment, not a code block.
|
|
128
|
+
const markerIdx = body.indexOf('<!-- magpie:finding')
|
|
129
|
+
expect(markerIdx).toBeGreaterThan(-1)
|
|
130
|
+
// After the marker the body must be balanced; no dangling open fence above it.
|
|
131
|
+
const beforeMarker = body.slice(0, markerIdx)
|
|
132
|
+
const fenceCount = (beforeMarker.match(/```/g) ?? []).length
|
|
133
|
+
expect(fenceCount % 2).toBe(0)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('formatInlineBody handles a fenced suggestion that contains no surrounding prose', () => {
|
|
137
|
+
const body = formatInlineBody({
|
|
138
|
+
...findingA,
|
|
139
|
+
line: 12,
|
|
140
|
+
suggestion: {
|
|
141
|
+
body: '```ts\nconst safe = true;\n```',
|
|
142
|
+
startLine: 12,
|
|
143
|
+
endLine: 12,
|
|
144
|
+
},
|
|
145
|
+
} as never)
|
|
146
|
+
expect(body).toContain('```suggestion\nconst safe = true;\n```')
|
|
147
|
+
// No stray, unterminated fence above the magpie marker.
|
|
148
|
+
const markerIdx = body.indexOf('<!-- magpie:finding')
|
|
149
|
+
const beforeMarker = body.slice(0, markerIdx)
|
|
150
|
+
expect((beforeMarker.match(/```/g) ?? []).length % 2).toBe(0)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('buildSuggestion uses a longer outer fence when the code body contains triple backticks', () => {
|
|
154
|
+
const body = formatInlineBody({
|
|
155
|
+
...findingA,
|
|
156
|
+
line: 12,
|
|
157
|
+
suggestion: {
|
|
158
|
+
body: 'const md = "```inline```"',
|
|
159
|
+
startLine: 12,
|
|
160
|
+
endLine: 12,
|
|
161
|
+
},
|
|
162
|
+
} as never)
|
|
163
|
+
// 3 backticks in body → outer fence must be at least 4 to avoid premature close.
|
|
164
|
+
expect(body).toContain('````suggestion\nconst md = "```inline```"\n````')
|
|
165
|
+
})
|
|
166
|
+
|
|
112
167
|
test('runPost rejects when pr.json is missing', async () => {
|
|
113
168
|
const outcome = await runPost({ runDir, findingIds: ['anything'] })
|
|
114
169
|
expect(outcome.ok).toBe(false)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
1
|
+
import { beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { Highlighter } from 'shiki'
|
|
3
|
+
import { getHighlighter } from '../highlight.ts'
|
|
2
4
|
import { renderAnnotation } from '../render-annotation.ts'
|
|
3
5
|
import type { ReviewFinding } from '../types.ts'
|
|
4
6
|
|
|
@@ -14,54 +16,171 @@ const finding: ReviewFinding = {
|
|
|
14
16
|
suggestion: { body: 'await lock.acquire()', startLine: 42, endLine: 42 },
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
let hl: Highlighter
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
hl = await getHighlighter()
|
|
22
|
+
})
|
|
23
|
+
|
|
17
24
|
describe('renderAnnotation', () => {
|
|
18
25
|
test('emits a div with data-finding-id and severity class', () => {
|
|
19
|
-
const html = renderAnnotation(finding, {
|
|
26
|
+
const html = renderAnnotation(finding, {
|
|
27
|
+
checked: false,
|
|
28
|
+
posted: false,
|
|
29
|
+
asCard: false,
|
|
30
|
+
highlighter: hl,
|
|
31
|
+
})
|
|
20
32
|
expect(html).toContain(`data-finding-id="f-1"`)
|
|
21
33
|
expect(html).toContain('annot')
|
|
22
34
|
expect(html).toContain('data-severity="high"')
|
|
23
35
|
})
|
|
24
36
|
|
|
25
37
|
test('renders severity label, domain chip, and title', () => {
|
|
26
|
-
const html = renderAnnotation(finding, {
|
|
38
|
+
const html = renderAnnotation(finding, {
|
|
39
|
+
checked: false,
|
|
40
|
+
posted: false,
|
|
41
|
+
asCard: false,
|
|
42
|
+
highlighter: hl,
|
|
43
|
+
})
|
|
27
44
|
expect(html).toContain('HIGH')
|
|
28
45
|
expect(html).toContain('Bugs')
|
|
29
46
|
expect(html).toContain('Race in cache.refresh()')
|
|
30
47
|
})
|
|
31
48
|
|
|
32
49
|
test('renders parsed description sections', () => {
|
|
33
|
-
const html = renderAnnotation(finding, {
|
|
50
|
+
const html = renderAnnotation(finding, {
|
|
51
|
+
checked: false,
|
|
52
|
+
posted: false,
|
|
53
|
+
asCard: false,
|
|
54
|
+
highlighter: hl,
|
|
55
|
+
})
|
|
34
56
|
expect(html).toContain('Observation')
|
|
35
57
|
expect(html).toContain('two refreshes overlap')
|
|
36
58
|
expect(html).toContain('Why it matters')
|
|
37
59
|
})
|
|
38
60
|
|
|
39
61
|
test('renders the suggestion code block when present', () => {
|
|
40
|
-
const html = renderAnnotation(finding, {
|
|
62
|
+
const html = renderAnnotation(finding, {
|
|
63
|
+
checked: false,
|
|
64
|
+
posted: false,
|
|
65
|
+
asCard: false,
|
|
66
|
+
highlighter: hl,
|
|
67
|
+
})
|
|
41
68
|
expect(html).toContain('<pre')
|
|
42
|
-
|
|
69
|
+
// Shiki tokenizes identifiers into separate spans; check for a known token
|
|
70
|
+
expect(html).toContain('acquire')
|
|
43
71
|
})
|
|
44
72
|
|
|
45
73
|
test('renders risk dimensions footer', () => {
|
|
46
|
-
const html = renderAnnotation(finding, {
|
|
74
|
+
const html = renderAnnotation(finding, {
|
|
75
|
+
checked: false,
|
|
76
|
+
posted: false,
|
|
77
|
+
asCard: false,
|
|
78
|
+
highlighter: hl,
|
|
79
|
+
})
|
|
47
80
|
expect(html).toContain('Impact: <span class="tag">high</span>')
|
|
48
81
|
expect(html).toContain('Likelihood: <span class="tag">likely</span>')
|
|
49
82
|
})
|
|
50
83
|
|
|
51
84
|
test('marks posted findings and disables checkbox', () => {
|
|
52
|
-
const html = renderAnnotation(finding, {
|
|
85
|
+
const html = renderAnnotation(finding, {
|
|
86
|
+
checked: true,
|
|
87
|
+
posted: true,
|
|
88
|
+
asCard: false,
|
|
89
|
+
highlighter: hl,
|
|
90
|
+
})
|
|
53
91
|
expect(html).toContain('data-posted="true"')
|
|
54
92
|
expect(html).toContain('checked disabled')
|
|
55
93
|
})
|
|
56
94
|
|
|
57
95
|
test('marks suggestion-only findings with data-suggestion="true"', () => {
|
|
58
96
|
const suggestion: ReviewFinding = { ...finding, risk: { ...finding.risk, action: 'optional' } }
|
|
59
|
-
const html = renderAnnotation(suggestion, {
|
|
97
|
+
const html = renderAnnotation(suggestion, {
|
|
98
|
+
checked: false,
|
|
99
|
+
posted: false,
|
|
100
|
+
asCard: false,
|
|
101
|
+
highlighter: hl,
|
|
102
|
+
})
|
|
60
103
|
expect(html).toContain('data-suggestion="true"')
|
|
61
104
|
})
|
|
62
105
|
|
|
63
106
|
test('asCard=true wraps in .issue-card', () => {
|
|
64
|
-
const html = renderAnnotation(finding, {
|
|
107
|
+
const html = renderAnnotation(finding, {
|
|
108
|
+
checked: false,
|
|
109
|
+
posted: false,
|
|
110
|
+
asCard: true,
|
|
111
|
+
highlighter: hl,
|
|
112
|
+
})
|
|
65
113
|
expect(html).toContain('class="issue-card')
|
|
66
114
|
})
|
|
115
|
+
|
|
116
|
+
test('renders backtick spans in description as <code class="inline-code">', () => {
|
|
117
|
+
const f: ReviewFinding = {
|
|
118
|
+
id: 'f-2',
|
|
119
|
+
file: 'a.ts',
|
|
120
|
+
line: 1,
|
|
121
|
+
severity: 'high',
|
|
122
|
+
title: 't',
|
|
123
|
+
description: 'Observation: `scheduler.ts` wires `.finally(...)` only; no `.catch(...)`.',
|
|
124
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
125
|
+
domain: 'bugs',
|
|
126
|
+
}
|
|
127
|
+
const html = renderAnnotation(f, {
|
|
128
|
+
highlighter: hl,
|
|
129
|
+
checked: false,
|
|
130
|
+
posted: false,
|
|
131
|
+
asCard: true,
|
|
132
|
+
})
|
|
133
|
+
expect(html).toContain('<code class="inline-code">scheduler.ts</code>')
|
|
134
|
+
expect(html).toContain('<code class="inline-code">.finally(...)</code>')
|
|
135
|
+
expect(html).toContain('<code class="inline-code">.catch(...)</code>')
|
|
136
|
+
expect(html).not.toContain('<scheduler.ts')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('escapes HTML inside backtick spans', () => {
|
|
140
|
+
const f: ReviewFinding = {
|
|
141
|
+
id: 'f-3',
|
|
142
|
+
file: 'a.ts',
|
|
143
|
+
line: 1,
|
|
144
|
+
severity: 'low',
|
|
145
|
+
title: 't',
|
|
146
|
+
description: 'Observation: `<Foo & Bar>` should not break out.',
|
|
147
|
+
risk: { impact: 'low', likelihood: 'edge-case', confidence: 'low', action: 'consider' },
|
|
148
|
+
domain: 'code-smells',
|
|
149
|
+
}
|
|
150
|
+
const html = renderAnnotation(f, {
|
|
151
|
+
highlighter: hl,
|
|
152
|
+
checked: false,
|
|
153
|
+
posted: false,
|
|
154
|
+
asCard: true,
|
|
155
|
+
})
|
|
156
|
+
expect(html).toContain('<code class="inline-code"><Foo & Bar></code>')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('renders suggestion body with shiki highlighting', () => {
|
|
160
|
+
const f: ReviewFinding = {
|
|
161
|
+
id: 'f-1',
|
|
162
|
+
file: 'scheduler.ts',
|
|
163
|
+
line: 12,
|
|
164
|
+
severity: 'high',
|
|
165
|
+
title: 'unhandled rejection',
|
|
166
|
+
description: 'Observation: scheduler drops errors.',
|
|
167
|
+
risk: { impact: 'high', likelihood: 'possible', confidence: 'high', action: 'should-fix' },
|
|
168
|
+
domain: 'bugs',
|
|
169
|
+
suggestion: {
|
|
170
|
+
body: 'const x: number = 1\nconsole.log(x)',
|
|
171
|
+
startLine: 1,
|
|
172
|
+
endLine: 2,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
const html = renderAnnotation(f, {
|
|
176
|
+
highlighter: hl,
|
|
177
|
+
checked: false,
|
|
178
|
+
posted: false,
|
|
179
|
+
asCard: true,
|
|
180
|
+
})
|
|
181
|
+
expect(html).toContain('class="suggestion shiki')
|
|
182
|
+
expect(html).toContain('<span class="line">')
|
|
183
|
+
// Shiki tokenizes identifiers separately, so check for the token text
|
|
184
|
+
expect(html).toContain('console')
|
|
185
|
+
})
|
|
67
186
|
})
|