@litfamily/litclaude 1.0.29 → 1.0.31
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/CHANGELOG.md +9 -0
- package/README.md +8 -8
- package/README_ko-KR.md +8 -8
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +19 -8
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/lit-mod.mjs +43 -4
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +6 -6
- package/plugins/litclaude/lib/deliverable-hedge-guard.mjs +8 -5
- package/plugins/litclaude/lib/mod/native-card.mjs +133 -0
- package/plugins/litclaude/lib/mod/svg-card.mjs +24 -27
- package/plugins/litclaude/skills/lit-humanizer/SKILL.md +1 -1
- package/plugins/litclaude/skills/lit-humanizer/references/taxonomy.md +1 -1
- package/plugins/litclaude/skills/lit-humanizer/rules.json +10 -7
- package/plugins/litclaude/skills/lit-humanizer/scripts/core.mjs +13 -2
- package/plugins/litclaude/skills/lit-humanizer/scripts/scan-input.mjs +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.31 - 2026-10-17
|
|
4
|
+
|
|
5
|
+
- In the Claude Code desktop app, the mod card used to blink: every real change, such as a new step, tool or skill, swapped in a whole new page, so the card went blank for a moment and its animations started over. The desktop card is now built from the app's own boxes and text, and a change updates only what moved. It looks the same but no longer animates. The terminal and phone cards are unchanged.
|
|
6
|
+
|
|
7
|
+
## 1.0.30 - 2026-10-16
|
|
8
|
+
|
|
9
|
+
- The pre-write guard now refuses more of the lines that tell a reader what a document is not for, such as `This table is not a ranking.` or `이 문서는 제품을 비교하려는 것이 아닙니다.` These forms came from real sessions. Bold-label callouts such as `> **주의:** ...` are now read too, while ordinary quoted text is still skipped. Lines from manuals and lab reports that only look similar stay allowed, and a large flood of matches can no longer push the scan past its limit into an allowed write.
|
|
10
|
+
- In the Claude Code desktop app, the mod card was sent again every two seconds while a tool ran, because its elapsed clock changed. The desktop card now leaves the clock out and redraws only when something it shows changes; the terminal card keeps its live clock.
|
|
11
|
+
|
|
3
12
|
## 1.0.29 - 2026-10-15
|
|
4
13
|
|
|
5
14
|
- Reports and decks no longer fill up with lines that tell the reader what the document is not for, such as `동일 조건의 독립 성능 비교로 쓰지 않음.` When the model writes one into a file, the pre-write guard refuses the save and the model rewrites it. Three new rules cover Korean self-referencing pledges, repeated disclaimers and the English forms; legal and financial notices, safety warnings and real negative findings stay untouched. The rules finish in milliseconds even on very long runs of spaces.
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitClaude robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">A Claude Code plugin that helps every session plan the work, build it, check it, and leave a note the next session can pick up.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/README_ko-KR.md">한국어</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Add `lit` to a Claude Code prompt and Claude pins the goal, writes a failing test first,
|
|
@@ -73,7 +73,7 @@ the `/plugin` menu.
|
|
|
73
73
|
## More
|
|
74
74
|
|
|
75
75
|
- [Full guide and skills gallery on GitHub](https://github.com/wjgoarxiv/litclaude#readme)
|
|
76
|
-
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
78
|
-
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
79
|
-
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
+
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/privacy.md)
|
|
77
|
+
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/migration.md)
|
|
78
|
+
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/CHANGELOG.md)
|
|
79
|
+
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE)
|
package/README_ko-KR.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">Claude Code에서 세션마다 계획하고, 만들고, 확인한 뒤 다음 세션이 이어받을 기록을 남기게 돕는 플러그인입니다.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/README.md">English</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Claude Code 프롬프트에 `lit`을 붙이면 Claude가 목표를 고정하고, 실패하는 테스트를 먼저 쓰고,
|
|
@@ -71,7 +71,7 @@ LitClaude는 프로젝트 상태를 `.litclaude/`에 두고, 그 안의 `.gitign
|
|
|
71
71
|
## 더 보기
|
|
72
72
|
|
|
73
73
|
- [전체 안내와 스킬 갤러리 (GitHub)](https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md)
|
|
74
|
-
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
75
|
-
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
-
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
74
|
+
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/privacy.md)
|
|
75
|
+
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/migration.md)
|
|
76
|
+
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/CHANGELOG.md)
|
|
77
|
+
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.31"><title>release: 1.0.31</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.31</text></g></svg>
|
package/docs/hooks.md
CHANGED
|
@@ -462,14 +462,25 @@ step's spinner (`- \ | /`) and clock are ASCII, and a glyph swaps only together
|
|
|
462
462
|
|
|
463
463
|
The desktop app's Code tab, and the other surfaces that are not a terminal, draw a mod's `Text` in
|
|
464
464
|
a proportional font and cut each span to the cells it was laid out for, so the card above comes
|
|
465
|
-
apart there.
|
|
466
|
-
(`lib/mod/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
465
|
+
apart there. In the desktop app the mod builds the same five rows from the app's own boxes and
|
|
466
|
+
text instead (`lib/mod/native-card.mjs`): a dark card with a rounded border, the LIT pill, the
|
|
467
|
+
stage pills, the running tool, the skill chips and the usage figures, each row laid out by the app
|
|
468
|
+
in its monospaced face. The app updates that card in place when something changes, so a new stage,
|
|
469
|
+
tool or skill no longer blanks it. It holds still: the app's boxes and text have no animation, and
|
|
470
|
+
the card shows the settled state with no entrances or crossfades.
|
|
471
|
+
|
|
472
|
+
On a phone the card is one `Svg` drawing (`lib/mod/svg-card.mjs`): monospace text placed by
|
|
473
|
+
counted cells, real pills and a rounded border. While LitClaude works it moves with SMIL animation
|
|
474
|
+
inside the drawing: the pill and the border breathe and the step's spinner turns, and on `full` a
|
|
475
|
+
light crosses the current stage and the current chips, a head runs the rail leaving it, the count
|
|
476
|
+
bar's head shimmers and a comet runs around the border. An idle card is a still image.
|
|
477
|
+
|
|
478
|
+
Neither the desktop card nor the phone drawing has the step clock or the loop countdown: a new
|
|
479
|
+
drawing restarts the phone's animation, and the desktop card would only show a frozen clock. Both
|
|
480
|
+
name the running tool and show the loop round, and are redrawn only when something you would
|
|
481
|
+
notice changes, such as the stage, the tool, a skill, a usage figure or the goal. While the card
|
|
482
|
+
shows in a terminal and the app at once, the terminal's loop countdown and lane times wait for such
|
|
483
|
+
a change too; its step clock keeps counting on its own.
|
|
473
484
|
|
|
474
485
|
Options, in `/config` under LitClaude (stored in `pluginConfigs` in `~/.claude/settings.json`,
|
|
475
486
|
which the terminal and the desktop app's Code tab both read). The installer writes each default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litclaude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Claude Code plugin: add lit to a prompt and Claude plans, tests, checks and reviews the work, then leaves a handoff for the next session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { composeBand, composeEvidence } from "../lib/mod/frame.mjs";
|
|
16
16
|
import { TIMINGS } from "../lib/mod/motion.mjs";
|
|
17
|
+
import { composeNativeCard } from "../lib/mod/native-card.mjs";
|
|
17
18
|
import { renderBand, renderRows } from "../lib/mod/render.mjs";
|
|
18
19
|
import { accentFromSettings, normalizeModSettings, resolveGlyphs } from "../lib/mod/settings.mjs";
|
|
19
20
|
import { composeSvgCard } from "../lib/mod/svg-card.mjs";
|
|
@@ -50,6 +51,8 @@ const LONG_TURN_MS = 3 * 60 * 1000;
|
|
|
50
51
|
|
|
51
52
|
const createLive = (options) => ({
|
|
52
53
|
band: normalizeModSettings(options).band,
|
|
54
|
+
motion: normalizeModSettings(options).motion,
|
|
55
|
+
drawnOn: {},
|
|
53
56
|
iconFont: { present: false, path: null, note: "basic: not checked yet" },
|
|
54
57
|
env: {},
|
|
55
58
|
snapshot: null,
|
|
@@ -90,6 +93,29 @@ const stateIo = ($) => ({
|
|
|
90
93
|
|
|
91
94
|
const toastText = (raised) => `🔥 ${raised.title}${raised.detail ? ` · ${raised.detail}` : ""}`;
|
|
92
95
|
|
|
96
|
+
// The model less what moves with time alone: the step clock, the loop countdown, the lanes' run times.
|
|
97
|
+
const stillModel = (model) => model && {
|
|
98
|
+
...model,
|
|
99
|
+
step: model.step && { name: model.step.name },
|
|
100
|
+
autoloop: model.autoloop && { n: model.autoloop.n, max: model.autoloop.max },
|
|
101
|
+
lanes: { ...model.lanes, list: model.lanes.list.map(({ elapsedMs, ...lane }) => lane) },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// What the card would show on each surface still attached. Invalidation redraws every surface at
|
|
105
|
+
// once. A phone's Svg is loaded afresh with each new source, restarting its animations, and the
|
|
106
|
+
// desktop's own-element card shows no clock; so while either is attached, time alone never
|
|
107
|
+
// redraws: the Svg is compared by its drawing, the desktop and the terminal by the still model (the
|
|
108
|
+
// terminal's surface module counts the step clock on by itself). A terminal on its own is compared
|
|
109
|
+
// by the whole model, so its countdown and lane times move too.
|
|
110
|
+
function drawnKey(live, model) {
|
|
111
|
+
const kinds = new Set(Object.values(live.drawnOn));
|
|
112
|
+
if (kinds.size === 0) return JSON.stringify(model);
|
|
113
|
+
const still = kinds.has("svg") || kinds.has("native");
|
|
114
|
+
return [...kinds].map((kind) => (kind === "svg"
|
|
115
|
+
? (model?.visible ? composeSvgCard(model, { motion: live.motion, accent: live.accent }).source : "hidden")
|
|
116
|
+
: JSON.stringify(still ? stillModel(model) : model))).join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
async function rebuild($, live) {
|
|
94
120
|
const now = await $.clock.now();
|
|
95
121
|
const previous = live.model;
|
|
@@ -117,7 +143,7 @@ async function rebuild($, live) {
|
|
|
117
143
|
$.ui.invalidate("ui.render");
|
|
118
144
|
});
|
|
119
145
|
}
|
|
120
|
-
if (
|
|
146
|
+
if (drawnKey(live, previous) !== drawnKey(live, next)) $.ui.invalidate("ui.render");
|
|
121
147
|
return next;
|
|
122
148
|
}
|
|
123
149
|
|
|
@@ -328,6 +354,14 @@ export const register = (on, options) => {
|
|
|
328
354
|
return next(e);
|
|
329
355
|
}).catch(($, e, next) => next(e));
|
|
330
356
|
|
|
357
|
+
// A client left: once no client of its surface remains, that surface stops counting.
|
|
358
|
+
on("session.detach", async ($, e, next) => {
|
|
359
|
+
const result = await next(e);
|
|
360
|
+
const roster = await $.session.surfaces();
|
|
361
|
+
if (!roster.includes(e.surface)) delete live.drawnOn[e.surface];
|
|
362
|
+
return result;
|
|
363
|
+
}).catch(($, e, next) => next(e));
|
|
364
|
+
|
|
331
365
|
on("ui.fault", async ($, e, next) => {
|
|
332
366
|
if (e.component === "AbovePrompt") live.clientFailed = true;
|
|
333
367
|
return next(e);
|
|
@@ -341,9 +375,14 @@ export const register = (on, options) => {
|
|
|
341
375
|
const elements = $.ui.resolve(e);
|
|
342
376
|
const columns = Math.max(20, e.props.bodyColumns ?? e.viewport?.columns ?? 80);
|
|
343
377
|
let drawn = null;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
378
|
+
const native = e.surface === "desktop";
|
|
379
|
+
live.drawnOn[e.surface] = native ? "native" : e.surface !== "terminal" && elements.Svg ? "svg" : "cells";
|
|
380
|
+
if (native) {
|
|
381
|
+
// The desktop draws Text in a proportional font, so the cell-built card comes apart there, and
|
|
382
|
+
// it loads each new Svg into a fresh frame, blanking it; it gets a card of flex rows instead.
|
|
383
|
+
if (model.visible) drawn = composeNativeCard(elements, model, { accent: live.accent });
|
|
384
|
+
} else if (e.surface !== "terminal" && elements.Svg) {
|
|
385
|
+
// A phone or editor draws Text in a proportional font too; it gets the card as one drawing.
|
|
347
386
|
if (model.visible) {
|
|
348
387
|
const { source, alt, animated } = composeSvgCard(model, { motion: settings.motion, accent: live.accent });
|
|
349
388
|
drawn = elements.Svg({ source, alt, ...(animated ? { isInteractive: true } : {}) });
|
|
@@ -7,7 +7,7 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
7
7
|
["lib/canonical-frontend-corpus.mjs", "a03490b0d7b136fb5efe86d37704e9b4f945c462530db5d51bbfd5b1c70c8b2c"],
|
|
8
8
|
["lib/canonical-runtime-closures.mjs", "80e7512135240ee9a47818412a578a1a7db0800cbab8a9721e5efa0421292bf5"],
|
|
9
9
|
["lib/canonical-runtime-commitments.mjs", "dc92035af3ef5fc61459d8feae25868cdbee9819d7b70f344de92c1a16d3cddd"],
|
|
10
|
-
["lib/deliverable-hedge-guard.mjs", "
|
|
10
|
+
["lib/deliverable-hedge-guard.mjs", "266086125959cd77501f563442bfe68b75fe197907224a46023fe4fac5b7c9f2"],
|
|
11
11
|
["lib/durable-plan.mjs", "33fc7c2ba6305602c73650c8f29e6141e93b8ac32a444f772807e11bf8c22e44"],
|
|
12
12
|
["lib/immutable-expected-file-map.mjs", "4061486bc8eda8a53e56b7b346a1e400e7c858c0a682c83362fea460b46dfd4b"],
|
|
13
13
|
["lib/lit-mark.mjs", "cb57921a3da0ff983c74e9426dcd3b2379db090198743139e379a059c128021e"],
|
|
@@ -557,7 +557,7 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
557
557
|
["skills/lit-docx/templates/tonalities/proposal.yaml", "b1b7f4875a89208939ca82d69e6cfb30c35b64ea91d67d5aecc98fe04bde3bfc"],
|
|
558
558
|
["skills/lit-docx/templates/tonalities/report.yaml", "23ac1d24f8936c6d522e19354d5948f0d00dce6d60c34fa42e37adafcc68cb8c"],
|
|
559
559
|
["skills/lit-humanizer/NOTICE", "c2608d2bc42cac22e77ac64eb137f8fc40d8f9a0b08ae7d7a8a75cf11d9289be"],
|
|
560
|
-
["skills/lit-humanizer/SKILL.md", "
|
|
560
|
+
["skills/lit-humanizer/SKILL.md", "5cf7f622bfd651d989a966a7b3bbd18a751ef6ecf51b4edbf68c571c012eb285"],
|
|
561
561
|
["skills/lit-humanizer/assets/always-on-rule.md", "1c0e1d9724f012f05142c69d06e8a8cc89596556cba2c8ff1438e99c7848a5cf"],
|
|
562
562
|
["skills/lit-humanizer/assets/report-skeleton.md", "e641dae43c1c13398a13f56c7d404fb821cdb9e813bf5eff562c29cd55b7dcd0"],
|
|
563
563
|
["skills/lit-humanizer/assets/slide-text-skeleton.md", "489fd07c010beaa1ecc8e02fc77add03d42a12ef1fc90f964d6711c51286cfd7"],
|
|
@@ -617,14 +617,14 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
617
617
|
["skills/lit-humanizer/references/litclaude-rewrite-rules.md", "ba0a9de5d02a427a1dd955bfb71f215abf76d658a50a31e7d42d862b3db44870"],
|
|
618
618
|
["skills/lit-humanizer/references/litclaude-style-patterns.md", "19932e2dd5eda9e1e48f0d83086170ad4548e89d05333f702b1d342494c7af5e"],
|
|
619
619
|
["skills/lit-humanizer/references/rewrite-playbook.md", "3b0bae2e24e9aada73ea527402e80c25a9bf471b65871248858496f1a2fe8ffb"],
|
|
620
|
-
["skills/lit-humanizer/references/taxonomy.md", "
|
|
621
|
-
["skills/lit-humanizer/rules.json", "
|
|
620
|
+
["skills/lit-humanizer/references/taxonomy.md", "f962ce4cc3458d2c2874858501d81751110280f0b53a2fbf380e77412535a463"],
|
|
621
|
+
["skills/lit-humanizer/rules.json", "d79e5619f673912e032bcd54fd24ea299b1b2e22643fb128cbe522ad020cc6eb"],
|
|
622
622
|
["skills/lit-humanizer/scripts/artifact-text.mjs", "36f893449d0b7719c747cbb512e0b7caed4146cb97b87acf813b3d911577a0e6"],
|
|
623
|
-
["skills/lit-humanizer/scripts/core.mjs", "
|
|
623
|
+
["skills/lit-humanizer/scripts/core.mjs", "12b9086662ee4fb70056553acedbd052d8d0c4c41f363aa2affc81c19dedf4ad"],
|
|
624
624
|
["skills/lit-humanizer/scripts/detect.mjs", "cdec0aa65c61c36fc1d7fb78551029a5a362ff8693ba8e04a52b7366d5e42a36"],
|
|
625
625
|
["skills/lit-humanizer/scripts/extract_office_text.py", "98eb5507bb78b6d3bf0119da2c8de4a5613efb5806aeef487dff3b6161cd2a13"],
|
|
626
626
|
["skills/lit-humanizer/scripts/ko-metrics.mjs", "5e53d6dfb212241fbcbddaa5d89cacfda61fa739616b4d1a3dd4d76268b0d871"],
|
|
627
|
-
["skills/lit-humanizer/scripts/scan-input.mjs", "
|
|
627
|
+
["skills/lit-humanizer/scripts/scan-input.mjs", "504e6e12909a94710d249a97e56e8f632948cd5f1fe9bec4266bbed809d714df"],
|
|
628
628
|
["skills/lit-pptx/NOTICE", "3b8ee9072906635575aded1e2d2eed51dec1284af9f6ccd47151de376f541153"],
|
|
629
629
|
["skills/lit-pptx/SKILL.md", "7a7f7dadc4bf44ce9008a0b8120c8147b79859b1eb42b5861a249c3d45714521"],
|
|
630
630
|
["skills/lit-pptx/assets/azure/circle-lg.png", "20c10807b0bda06766bb82ab257d2a10f815337c828ee9c6d74852569779fee0"],
|
|
@@ -181,13 +181,15 @@ const runScanner = (artifacts, pluginRoot, execute = spawnSync) => {
|
|
|
181
181
|
try {
|
|
182
182
|
const parsed = JSON.parse(run.stdout);
|
|
183
183
|
if (!Array.isArray(parsed.findings)) return { failure: "returned an invalid result" };
|
|
184
|
-
|
|
184
|
+
const total = Number.isInteger(parsed.total) && parsed.total >= parsed.findings.length ? parsed.total : parsed.findings.length;
|
|
185
|
+
return { findings: parsed.findings, total };
|
|
185
186
|
} catch {
|
|
186
187
|
return { failure: "returned an invalid result" };
|
|
187
188
|
}
|
|
188
189
|
};
|
|
189
190
|
|
|
190
|
-
const resultFor = (findings, paths) => ({
|
|
191
|
+
const resultFor = (findings, paths, total = findings.length) => ({
|
|
192
|
+
total,
|
|
191
193
|
status: findings.some((finding) => finding.severity === "block")
|
|
192
194
|
? "block"
|
|
193
195
|
: findings.some((finding) => finding.severity === "warn") ? "warn" : "clean",
|
|
@@ -231,7 +233,7 @@ const evaluateDeliverableHedgeGuardInternal = ({ input, pluginRoot, phase = "pre
|
|
|
231
233
|
}
|
|
232
234
|
const scanned = runScanner(artifacts, pluginRoot, execute);
|
|
233
235
|
if (scanned.failure) return { status: "unavailable", reason: `detector ${scanned.failure}`, findings: [], paths: [] };
|
|
234
|
-
return { ...resultFor(scanned.findings, paths), phase };
|
|
236
|
+
return { ...resultFor(scanned.findings, paths, scanned.total), phase };
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
const { artifacts: candidates, skipped } = toolArtifacts(input);
|
|
@@ -272,7 +274,7 @@ const evaluateDeliverableHedgeGuardInternal = ({ input, pluginRoot, phase = "pre
|
|
|
272
274
|
const artifacts = [...byPath].map(([path, text]) => ({ path, text }));
|
|
273
275
|
const scanned = runScanner(artifacts, pluginRoot, execute);
|
|
274
276
|
if (scanned.failure) return { status: "unavailable", reason: `detector ${scanned.failure}`, findings: [], paths: [] };
|
|
275
|
-
return resultFor(scanned.findings, artifacts.map(({ path }) => path));
|
|
277
|
+
return resultFor(scanned.findings, artifacts.map(({ path }) => path), scanned.total);
|
|
276
278
|
};
|
|
277
279
|
|
|
278
280
|
export const evaluateDeliverableHedgeGuard = (options) => {
|
|
@@ -286,7 +288,8 @@ export const evaluateDeliverableHedgeGuard = (options) => {
|
|
|
286
288
|
const findingSummary = (result) => {
|
|
287
289
|
const findings = Array.isArray(result?.findings) ? result.findings : [];
|
|
288
290
|
const listed = findings.slice(0, 8).map((finding) => `${finding.rule} line ${finding.line}`).join(", ");
|
|
289
|
-
const
|
|
291
|
+
const total = Number.isInteger(result?.total) ? Math.max(result.total, findings.length) : findings.length;
|
|
292
|
+
const omitted = total > 8 ? `; ${total - 8} more` : "";
|
|
290
293
|
return listed ? `${listed}${omitted}` : "pattern review needed";
|
|
291
294
|
};
|
|
292
295
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The LIT card in the desktop app's own elements: a bordered Box of flex rows and monospaced Text
|
|
2
|
+
// spans, laid out by the app rather than by counted cells. The app draws a Box as a flex div and a
|
|
3
|
+
// Text as a styled span (the host's StyledElement), so a new tree updates the page in place. The Svg
|
|
4
|
+
// card it replaces was a whole document the app loaded into a fresh sandboxed frame on every real
|
|
5
|
+
// change (the stage, the tool, a skill), which blanked the card and restarted its animations.
|
|
6
|
+
//
|
|
7
|
+
// It shows the same five rows and colours as the Svg card (svg-card.mjs): the brand pill, the
|
|
8
|
+
// discipline and the stage indicator; the workflow map or the phase track with the lanes and
|
|
9
|
+
// progress; the detail line with the running tool; the skill chips, the notice or the finish; the
|
|
10
|
+
// usage figures. Box and Text carry no animation props, so this card is still; like the Svg card
|
|
11
|
+
// it carries no step clock and no loop countdown, so time alone never changes the tree.
|
|
12
|
+
|
|
13
|
+
import { barCells } from "./motion.mjs";
|
|
14
|
+
import { C, LABELS, PHASES, TONES, VERDICT_FILL, borderColor, indicatorOf, levelColor, mapColor, rightOfPhases } from "./svg-card.mjs";
|
|
15
|
+
|
|
16
|
+
const MAX_CHIPS = 6;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The card as one element tree from a surface's `{ Box, Text }`. `accent` is the HUD accent.
|
|
20
|
+
*/
|
|
21
|
+
export const composeNativeCard = ({ Box, Text }, model, { accent = "#5fd7ff" } = {}) => {
|
|
22
|
+
const span = (body, { fill = C.white, bold = false, bg = null } = {}) => Text({
|
|
23
|
+
children: body,
|
|
24
|
+
color: fill,
|
|
25
|
+
mono: true,
|
|
26
|
+
wrap: "truncate-end",
|
|
27
|
+
...(bg ? { backgroundColor: bg } : {}),
|
|
28
|
+
...(bold ? { weight: "semibold" } : {}),
|
|
29
|
+
});
|
|
30
|
+
const pill = (label, fill, ink = C.ink, bold = true) => span(` ${label} `, { fill: ink, bg: fill, bold });
|
|
31
|
+
const pieces = (list) => list.map(([body, style]) => span(body, style));
|
|
32
|
+
const group = (children, gap = "xs") => Box({ flexDirection: "row", alignItems: "center", gap, flexShrink: 1, overflow: "hidden", children });
|
|
33
|
+
const joined = (groups) => groups.flatMap((list, index) => (index > 0 ? [span("·", { fill: C.faint }), ...pieces(list)] : pieces(list)));
|
|
34
|
+
const row = (left, right = []) => Box({
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
justifyContent: "space-between",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: "md",
|
|
39
|
+
children: [group(left.length ? left : [span(" ")]), ...(right.length ? [group(right)] : [])],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const indicator = indicatorOf(model, accent);
|
|
43
|
+
const top = row(
|
|
44
|
+
[pill("🔥 LIT", C.pink), span("·", { fill: C.dim }), span(model.title ?? "ready", model.idle ? { fill: C.dim } : { fill: accent, bold: true })],
|
|
45
|
+
[span(indicator.label, { fill: indicator.fill, bold: !indicator.plain })],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const nowFill = (isWork = true) => (model.idle ? C.muted : model.ctxWarning && isWork ? C.amber : accent);
|
|
49
|
+
const nowInk = model.idle ? C.mutedText : C.ink;
|
|
50
|
+
let track;
|
|
51
|
+
if (model.workflow) {
|
|
52
|
+
const { stages } = model.workflow;
|
|
53
|
+
const last = Math.max(1, stages.length - 1);
|
|
54
|
+
const left = [];
|
|
55
|
+
stages.forEach((stage, index) => {
|
|
56
|
+
if (index > 0) left.push(span("──", { fill: stage.state === "todo" ? C.rail : mapColor(accent, (index - 0.5) / last) }));
|
|
57
|
+
if (stage.state === "now") left.push(pill(stage.label, nowFill(), nowInk));
|
|
58
|
+
else left.push(span(`${stage.label} ${stage.state === "done" ? "✓" : "·"}`, { fill: stage.state === "done" ? mapColor(accent, index / last) : C.dim }));
|
|
59
|
+
});
|
|
60
|
+
track = row(left, joined(rightOfPhases(model, false)));
|
|
61
|
+
} else {
|
|
62
|
+
const left = [];
|
|
63
|
+
PHASES.forEach((id, index) => {
|
|
64
|
+
const state = model.phases[id];
|
|
65
|
+
if (index > 0) left.push(span("──", { fill: model.phases[PHASES[index - 1]] === "done" ? accent : C.rail }));
|
|
66
|
+
if (state === "now") left.push(pill(LABELS[id], nowFill(id === "work"), nowInk));
|
|
67
|
+
else left.push(span(`${state === "done" ? "●" : "○"} ${LABELS[id]}`, { fill: state === "done" ? accent : C.dim }));
|
|
68
|
+
});
|
|
69
|
+
track = row(left, joined(rightOfPhases(model)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const flow = model.workflow;
|
|
73
|
+
const detailLeft = [];
|
|
74
|
+
if (flow) {
|
|
75
|
+
detailLeft.push(span("└", { fill: C.dim }));
|
|
76
|
+
const parts = [];
|
|
77
|
+
if (flow.count) {
|
|
78
|
+
const { done, total, unit } = flow.count;
|
|
79
|
+
const bar = barCells(total > 0 ? done / total : 0, 8).map(({ glyph, color }) => span("▮", { fill: glyph === "█" ? color : C.empty }));
|
|
80
|
+
parts.push([span(unit, { fill: C.dim }), Box({ flexDirection: "row", children: [span(`${done}`, { bold: true }), span(`/${total}`, { fill: C.dim })] }), Box({ flexDirection: "row", children: bar })]);
|
|
81
|
+
}
|
|
82
|
+
if (flow.verdicts) parts.push(flow.verdicts.map(({ name, state }) => span(`${name} ${VERDICT_FILL[state][0]}`, { fill: VERDICT_FILL[state][1] })));
|
|
83
|
+
for (const item of flow.items) parts.push([span(item)]);
|
|
84
|
+
if (flow.next) parts.push([span("next", { fill: C.dim }), span("▸", { fill: accent }), span(flow.next)]);
|
|
85
|
+
parts.forEach((part, index) => detailLeft.push(...(index > 0 ? [span("·", { fill: C.faint })] : []), ...part));
|
|
86
|
+
}
|
|
87
|
+
const detail = row(detailLeft, model.step ? [span("◌", { fill: accent }), span(model.step.name)] : []);
|
|
88
|
+
|
|
89
|
+
let middle;
|
|
90
|
+
if (model.completion) {
|
|
91
|
+
middle = row([span("✦", { fill: C.pink }), span("COMPLETE", { bold: true }), span(`· ${model.completion.detail ?? ""}`, { fill: C.green })]);
|
|
92
|
+
} else if (model.notice) {
|
|
93
|
+
const [bar, title, icon] = TONES[model.notice.tone] ?? TONES.pink;
|
|
94
|
+
middle = row([span("▌", { fill: bar }), span(`${icon} ${model.notice.title}`, { fill: title, bold: true }), ...(model.notice.detail ? [span(`· ${model.notice.detail}`, { fill: C.dim })] : [])]);
|
|
95
|
+
} else if (model.skills.length === 0) {
|
|
96
|
+
if (!model.idle && model.next && !model.workflow?.next) middle = row([span("▸", { fill: accent }), span(model.next)]);
|
|
97
|
+
else if (!model.idle) middle = row([span("no LitFamily skill yet in this request", { fill: C.dim })]);
|
|
98
|
+
else middle = row([span("no LitFamily skill yet · type", { fill: C.dim }), span("lit <task>", { fill: accent }), span("to start one", { fill: C.dim })]);
|
|
99
|
+
} else {
|
|
100
|
+
const shown = model.skills.slice(0, MAX_CHIPS).map((chip) => (chip.state === "current"
|
|
101
|
+
? pill(`⚡ ${chip.id}`, accent)
|
|
102
|
+
: pill(`↯ ${chip.id}`, C.muted, C.mutedText, false)));
|
|
103
|
+
const rest = model.skills.length - MAX_CHIPS;
|
|
104
|
+
middle = row([span("skills", { fill: C.dim }), group(rest > 0 ? [...shown, pill(`+${rest}`, C.muted, C.dim, false)] : shown, "sm")]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bottomGroups = [];
|
|
108
|
+
const warning = model.warnings[0];
|
|
109
|
+
if (warning) {
|
|
110
|
+
const color = warning.tone === "red" ? C.red : C.amber;
|
|
111
|
+
bottomGroups.push([span(`${warning.tone === "red" ? "✗" : "⚠"} ${warning.subject}`, { fill: color, bold: true }), span(warning.text, { fill: color })]);
|
|
112
|
+
}
|
|
113
|
+
const { context, rates, git } = model.figures;
|
|
114
|
+
if (context !== null) {
|
|
115
|
+
const filled = Math.max(0, Math.min(5, Math.round(context / 20)));
|
|
116
|
+
const bar = Array.from({ length: 5 }, (_, i) => span("▮", { fill: i < filled ? accent : C.empty }));
|
|
117
|
+
bottomGroups.push([span("ctx", { fill: C.dim }), span(`${context}%`, { fill: accent }), Box({ flexDirection: "row", children: bar })]);
|
|
118
|
+
}
|
|
119
|
+
for (const rate of rates) bottomGroups.push([span(rate.label, { fill: C.dim }), span(`${rate.percent}%`, { fill: levelColor(rate.percent) })]);
|
|
120
|
+
if (git) bottomGroups.push([span("git", { fill: C.dim }), span(git, { fill: accent })]);
|
|
121
|
+
const bottom = row(bottomGroups.flatMap((list, index) => (index > 0 ? [span("·", { fill: C.faint }), ...list] : list)));
|
|
122
|
+
|
|
123
|
+
return Box({
|
|
124
|
+
flexDirection: "column",
|
|
125
|
+
gap: "xs",
|
|
126
|
+
paddingX: "md",
|
|
127
|
+
paddingY: "sm",
|
|
128
|
+
borderStyle: "round",
|
|
129
|
+
borderColor: borderColor(model, accent),
|
|
130
|
+
backgroundColor: C.bg,
|
|
131
|
+
children: [top, track, detail, middle, bottom],
|
|
132
|
+
});
|
|
133
|
+
};
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
// The LIT card as one SVG drawing, for the surfaces that are
|
|
2
|
-
// app
|
|
3
|
-
// terminal card's text border and its padded pills come apart
|
|
4
|
-
// element instead, and this module draws the same card into
|
|
5
|
-
// the stage indicator on top, the workflow map (or the phase
|
|
6
|
-
// places the session), the detail line with the live step, the
|
|
7
|
-
// finish), and the live figures at the bottom.
|
|
1
|
+
// The LIT card as one SVG drawing, for the surfaces that are neither a terminal grid nor the desktop
|
|
2
|
+
// app (which gets native-card.mjs). They draw a mod's Text in a proportional font and cut each span
|
|
3
|
+
// to the cells it was given, so the terminal card's text border and its padded pills come apart
|
|
4
|
+
// there. Those surfaces carry an `Svg` element instead, and this module draws the same card into
|
|
5
|
+
// it: the brand pill, the discipline and the stage indicator on top, the workflow map (or the phase
|
|
6
|
+
// track and progress when no family places the session), the detail line with the live step, the
|
|
7
|
+
// skill chips (or the notice or the finish), and the live figures at the bottom.
|
|
8
8
|
//
|
|
9
9
|
// It draws the settled state of the model; the terminal's entrances and crossfades stay terminal
|
|
10
10
|
// only. While LitClaude works and motion allows, SMIL animation in the drawing keeps it moving: the
|
|
11
11
|
// brand pill and the border breathe (full and subtle), a light crosses the current stage and this
|
|
12
12
|
// request's chips, a bright head runs along the rail leaving the current stage, the count bar's
|
|
13
13
|
// head shimmers, the step spinner turns and a comet runs around the border (full only). Idle, it is
|
|
14
|
-
// still. The step
|
|
14
|
+
// still. The drawing carries no step clock and no loop countdown: the surface loads each new
|
|
15
|
+
// drawing afresh, restarting every animation, so a figure that moves with time alone would make the
|
|
16
|
+
// card blink on every poll.
|
|
15
17
|
//
|
|
16
18
|
// Text is set in a monospace face and placed by counted cells (Hangul and emoji as two), so the
|
|
17
19
|
// layout does not depend on measuring a font the drawing cannot see.
|
|
18
20
|
|
|
19
|
-
import { formatClock } from "./frame.mjs";
|
|
20
21
|
import { textWidth, truncateToWidth } from "./layout.mjs";
|
|
21
22
|
import { TIMINGS, barCells, gradientAt } from "./motion.mjs";
|
|
22
|
-
import { formatDuration } from "./view.mjs";
|
|
23
23
|
|
|
24
|
-
const C = Object.freeze({
|
|
24
|
+
export const C = Object.freeze({
|
|
25
25
|
bg: "#0d1117",
|
|
26
26
|
pink: "#ff2d95",
|
|
27
27
|
orange: "#ff6337",
|
|
@@ -41,8 +41,8 @@ const C = Object.freeze({
|
|
|
41
41
|
hudYellow: "#d7af00",
|
|
42
42
|
hudRed: "#d75f5f",
|
|
43
43
|
});
|
|
44
|
-
const LABELS = Object.freeze({ plan: "PLAN", work: "WORK", review: "REVIEW" });
|
|
45
|
-
const PHASES = Object.freeze(["plan", "work", "review"]);
|
|
44
|
+
export const LABELS = Object.freeze({ plan: "PLAN", work: "WORK", review: "REVIEW" });
|
|
45
|
+
export const PHASES = Object.freeze(["plan", "work", "review"]);
|
|
46
46
|
|
|
47
47
|
// Drawing units: CSS pixels at the card's natural size; the surface draws it at that width, up to its slot.
|
|
48
48
|
export const SVG_CARD = Object.freeze({ width: 880, height: 160, font: 13, cell: 7.8, pad: 18 });
|
|
@@ -52,7 +52,7 @@ const FONT = "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, 'JetBrains Mono',
|
|
|
52
52
|
|
|
53
53
|
const esc = (text) => String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
54
54
|
const widthOf = (text) => textWidth(text) * SVG_CARD.cell;
|
|
55
|
-
const levelColor = (percent) => (percent >= 90 ? C.hudRed : percent >= 70 ? C.hudYellow : C.hudGreen);
|
|
55
|
+
export const levelColor = (percent) => (percent >= 90 ? C.hudRed : percent >= 70 ? C.hudYellow : C.hudGreen);
|
|
56
56
|
|
|
57
57
|
// Edge spaces become offsets: an SVG text element drops leading and trailing whitespace.
|
|
58
58
|
const text = (x, y, body, { fill = C.white, bold = false, anchor = "start" } = {}) => {
|
|
@@ -85,7 +85,7 @@ const pill = (x, y, label, { fill, ink, bold = true, sweep = false, breath = nul
|
|
|
85
85
|
return { markup: parts.join(""), end: x + w };
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
const indicatorOf = (model, accent) => {
|
|
88
|
+
export const indicatorOf = (model, accent) => {
|
|
89
89
|
if (model.completion) return { label: "DONE ✓", fill: C.green };
|
|
90
90
|
if (model.idle) return { label: "idle", fill: C.dim, plain: true };
|
|
91
91
|
const flow = model.workflow;
|
|
@@ -109,11 +109,11 @@ const topRow = (model, accent, animate) => {
|
|
|
109
109
|
return brand.markup + title.markup + right;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const rightOfPhases = (model, withProgress = true) => {
|
|
112
|
+
export const rightOfPhases = (model, withProgress = true) => {
|
|
113
113
|
const pieces = [];
|
|
114
114
|
if (model.ctxWarning) pieces.push([[`ctx ${model.ctxWarning.percent}% → ${model.ctxWarning.threshold}%`, { fill: C.amber, bold: true }]]);
|
|
115
115
|
if (model.lanes.count > 0) pieces.push([[`● ${model.lanes.count} ${model.lanes.count === 1 ? "lane" : "lanes"}`, { fill: C.green }]]);
|
|
116
|
-
if (model.autoloop) pieces.push([[`↻ ${model.autoloop.n}/${model.autoloop.max}`, { fill: C.violet }]
|
|
116
|
+
if (model.autoloop) pieces.push([[`↻ ${model.autoloop.n}/${model.autoloop.max}`, { fill: C.violet }]]);
|
|
117
117
|
if (!withProgress) return pieces;
|
|
118
118
|
if (model.progress) {
|
|
119
119
|
pieces.push([[`${model.progress.unit === "tasks" ? "tasks" : "goal"} `, { fill: C.dim }], [`${model.progress.done}`, { fill: C.white, bold: true }], [`/${model.progress.total}`, { fill: C.dim }]]);
|
|
@@ -158,7 +158,7 @@ const phaseRow = (model, accent, animate) => {
|
|
|
158
158
|
return out.join("");
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
const mapColor = (accent, position) => gradientAt([C.orange, C.pink, accent], position);
|
|
161
|
+
export const mapColor = (accent, position) => gradientAt([C.orange, C.pink, accent], position);
|
|
162
162
|
const shortLabel = (label) => label.replace(/^[A-Za-z]+/u, (word) => word.slice(0, 3));
|
|
163
163
|
|
|
164
164
|
// Right-aligned groups after `x`, the leftmost dropped first when the row is short of room.
|
|
@@ -218,9 +218,9 @@ const mapRowSvg = (model, accent, animate) => {
|
|
|
218
218
|
return out.join("");
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
-
const VERDICT_FILL = Object.freeze({ pass: ["✓", C.green], fail: ["✗", C.red], pending: ["·", C.dim], running: ["●", C.amber] });
|
|
221
|
+
export const VERDICT_FILL = Object.freeze({ pass: ["✓", C.green], fail: ["✗", C.red], pending: ["·", C.dim], running: ["●", C.amber] });
|
|
222
222
|
|
|
223
|
-
// └ task 3/7 ▮▮▮▯▯▯▯▯ · next ▸ <task> on the left, the spinner
|
|
223
|
+
// └ task 3/7 ▮▮▮▯▯▯▯▯ · next ▸ <task> on the left, the spinner and the tool on the right.
|
|
224
224
|
const detailRowSvg = (model, accent, animate) => {
|
|
225
225
|
const { width, pad, cell } = SVG_CARD;
|
|
226
226
|
const y = ROW_Y[2];
|
|
@@ -228,10 +228,7 @@ const detailRowSvg = (model, accent, animate) => {
|
|
|
228
228
|
const out = [];
|
|
229
229
|
let right = pad;
|
|
230
230
|
if (model.step) {
|
|
231
|
-
const
|
|
232
|
-
const end = width - pad;
|
|
233
|
-
out.push(text(end, y, clock, { fill: C.dim, anchor: "end" }));
|
|
234
|
-
const nameEnd = end - widthOf(clock);
|
|
231
|
+
const nameEnd = width - pad;
|
|
235
232
|
out.push(text(nameEnd, y, model.step.name, { fill: C.white, anchor: "end" }));
|
|
236
233
|
const cx = nameEnd - widthOf(model.step.name) - 10;
|
|
237
234
|
out.push(`<circle cx="${cx.toFixed(1)}" cy="${y - 4}" r="4.5" fill="none" stroke="${accent}" stroke-width="1.8" stroke-dasharray="18 10.3">`
|
|
@@ -279,7 +276,7 @@ const detailRowSvg = (model, accent, animate) => {
|
|
|
279
276
|
return out.join("");
|
|
280
277
|
};
|
|
281
278
|
|
|
282
|
-
const TONES = Object.freeze({ pink: [C.pink, "#ff6fae", "🔥"], amber: [C.amber, C.amber, "⚠"], red: [C.red, C.red, "✗"] });
|
|
279
|
+
export const TONES = Object.freeze({ pink: [C.pink, "#ff6fae", "🔥"], amber: [C.amber, C.amber, "⚠"], red: [C.red, C.red, "✗"] });
|
|
283
280
|
|
|
284
281
|
// Skill chips, or the notice or the finish while one is up, or the hint while nothing has fired.
|
|
285
282
|
const middleRow = (model, accent, animate) => {
|
|
@@ -354,7 +351,7 @@ const bottomRow = (model, accent) => {
|
|
|
354
351
|
return out.join("");
|
|
355
352
|
};
|
|
356
353
|
|
|
357
|
-
const borderColor = (model, accent) => {
|
|
354
|
+
export const borderColor = (model, accent) => {
|
|
358
355
|
if (model.warnings.some(({ tone }) => tone === "red")) return C.red;
|
|
359
356
|
if (model.warnings.length) return C.amber;
|
|
360
357
|
return model.idle ? C.faint : accent;
|
|
@@ -373,7 +370,7 @@ export const svgCardAlt = (model) => {
|
|
|
373
370
|
} else if (current && !model.idle) {
|
|
374
371
|
parts.push(`phase ${LABELS[current]}`);
|
|
375
372
|
}
|
|
376
|
-
if (model.step) parts.push(`running ${model.step.name}
|
|
373
|
+
if (model.step) parts.push(`running ${model.step.name}`);
|
|
377
374
|
if (model.skills.length) parts.push(`skills ${model.skills.map(({ id }) => id).join(", ")}`);
|
|
378
375
|
const { context, rates } = model.figures;
|
|
379
376
|
if (context !== null) parts.push(`context ${context}%`);
|
|
@@ -158,7 +158,7 @@ Review comments, README text, changelogs, commits, and pull-request prose as pro
|
|
|
158
158
|
|
|
159
159
|
- **Block** means a high-confidence drafting label, stacked disclaimer, or model self-reference needs a deliberate rewrite before the hook allows a supported reader-facing write.
|
|
160
160
|
- **Warn** means inspect tone, structure, or rhythm and decide from context. A warning never denies a write by itself.
|
|
161
|
-
- Fenced and inline code, Markdown blockquotes, internal records/paths, and unchanged replacement context are excluded. Tool adapters pass only new Write content or Edit/MultiEdit replacement text.
|
|
161
|
+
- Fenced and inline code, Markdown blockquotes, internal records/paths, and unchanged replacement context are excluded. One exception: a quote line that opens with a bold label (`> **주의:** ...`) is the author's own callout, and the three pledge rules read it as a single line; a plain quote stays excluded. Tool adapters pass only new Write content or Edit/MultiEdit replacement text.
|
|
162
162
|
- DOCX/PPTX and PDFs are read only after creation through bounded local extractors. If an extractor is missing, a file is unreadable, or the check times out, the guard allows the operation and shows one short note that the check did not run.
|
|
163
163
|
|
|
164
164
|
When a pattern would remove a correct fact, meaningful scope, required attribution, or the user's voice, keep the text and explain the decision only if the user needs it.
|
|
@@ -16,7 +16,7 @@ This original category comes from the 26 real lines in fixtures/pos-real.txt and
|
|
|
16
16
|
- en-demo-self-disclaimer — en, block. A demonstration label attached to a synthesis, assumption or recommendation is process framing. Name what is proposed and what was measured.
|
|
17
17
|
- ko-bracket-status-tag and en-bracket-status-tag — ko/en, block. Replace bracket keys with a sentence describing the status.
|
|
18
18
|
- ko-standalone-scope-disclaimer and en-standalone-scope-disclaimer — ko/en, block for stock disclaimers. Preserve a user-requested legal, medical or safety notice and place it once where it applies.
|
|
19
|
-
- ko-negative-pledge, ko-negative-pledge-repeat and en-negative-pledge — ko/en, block. A line that tells the reader what the document, table or number is not for (`동일 조건의 독립 성능 비교로 쓰지 않음`, `외부 공개용 수치가 아님`, `This is not intended as a head-to-head benchmark`) is a promise about the document, not a finding. ko-negative-pledge reads one line at a time and fires on
|
|
19
|
+
- ko-negative-pledge, ko-negative-pledge-repeat and en-negative-pledge — ko/en, block. A line that tells the reader what the document, table or number is not for (`동일 조건의 독립 성능 비교로 쓰지 않음`, `외부 공개용 수치가 아님`, `This is not intended as a head-to-head benchmark`) is a promise about the document, not a finding. ko-negative-pledge reads one line at a time and fires on a short list of shapes: a self-reference topic (이/본/해당/위/아래 plus a document noun) with a negated claim verb, a claim noun or a `비교하려는 것이 아님` intent clause, a comparison or ranking noun with a negated use or reading verb (`순위처럼 읽지 말아 주세요`), a purpose-용 data noun with 아님, numbers said to be incomparable or forbidden a speed verdict, ranking refused for this data, and a table's row order said not to be a ranking. A conditional method fact (`숫자와 문자열은 서로 비교할 수 없습니다`, `Do not compare these values directly; convert to SI first.`) is not one of them. ko-negative-pledge-repeat reads the whole text and fires when two such sentences end in 해석하지/주장하지/대표하지 않음 or a claim noun plus 아님. en-negative-pledge needs a comparison, benchmark or ranking noun, or `makes no claim`. Say it once as a fact beside the number it affects (`샘플은 한 공정 조건만 포함한다`), or move it to the reply. Operating rules (`방폭 구역에서 사용하지 않습니다`), negative findings (`첨가제 B는 수율을 개선하지 않았다`), and legal, financial or safety notices (대체하, 보장하, 권유 and `substitute for professional advice`) stay clean on purpose. The three rules also read a quote line that opens with a bold label (`> **주의:** 이 표는 순위가 아닙니다.`) as its own line, with the `>` blanked; a plain quote line stays unread. The guard scans only the text a write adds, so a second pledge appended with an Edit to a file that already holds one is not counted by the repeat rule.
|
|
20
20
|
- ko-status-negation, ko-mi-status, en-epistemic-negation and en-not-performed — warnings. ko-status-negation also reads `쓰지 않음` and the 않습니다/않았습니다 endings, with or without bold around the ending, so a method line such as `실제 운영 데이터로 검증하지 않았습니다` is flagged for a context check and never refused. A negative result or unperformed action may be the finding. Write the actor, scope and verb plainly; never turn unknown into false or an estimate into a fact.
|
|
21
21
|
- ko-plain-meta-label-warn and en-plain-caveat-label — warnings for boundary headings. They may be useful in internal or technical packets.
|
|
22
22
|
- en-knowledge-cutoff, en-ai-self-disclaimer, en-hope-helps and ko-chatbot-signoff — block. Remove the model wrapper or generic sign-off, then retain the answer.
|
|
@@ -884,14 +884,15 @@
|
|
|
884
884
|
"id": "ko-negative-pledge",
|
|
885
885
|
"lang": "ko",
|
|
886
886
|
"severity": "block",
|
|
887
|
-
"pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s
|
|
887
|
+
"pattern": "(?<![가-힣])(?:이|본|해당|위|아래)\\s+(?:자료|문서|보고서|슬라이드|덱|표|그림|그래프|차트|수치|결과|비교|분석|내용|예시)(?:은|는)\\s[^.!?\\n]{0,40}?(?:(?:해석하|의미하|주장하|대표하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?:비교|결론|벤치마크|순위|대표값|일반화)(?:이|가)|목적이|(?:위한|려는)\\s*것이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|순위표(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다|아니고)(?![가-힣]))|(?:비교|결론|벤치마크|순위|대표값|일반화)(?:처럼|으로|로)\\s*(?:[*_]{1,2}\\s*)?(?:쓰|사용하|해석하|읽|간주하|인용하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:공개|배포|대외|외부|홍보|발표|보고|비교|인용|공식)용\\s*(?:자료|문서|수치|표|그래프)(?:이|가)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?(?:은|는|이|가|을|를|끼리|도)(?![가-힣])(?:(?!면\\s)[^.!?\\n]){0,40}?(?<![와과]\\s)(?<![와과])비교할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음|나)(?![가-힣])|(?<![가-힣])(?:숫자|수치)(?:들)?끼리\\s*(?:서로\\s*|직접\\s*)?비교(?:해서|하여|해)\\s[^.!?\\n]{0,30}?[\"”’']\\s*(?:는|라는)\\s*결론[^.!?\\n]{0,8}?(?:수\\s*없(?:다|습니다)(?![가-힣])|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:(?<![가-힣])(?:숫자|수치)(?:들)?|(?<![가-힣])(?:이|위|아래|본)\\s+(?:표|값|자료|그래프|차트))[^.!?\\n]{0,40}?(?:빠르|느리)다고\\s*판단하(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?:빠르|느리)다[\"”’']?\\s*(?:는|라는)\\s*식으로\\s*읽(?:지\\s*(?:[*_]{1,2}\\s*)?(?:말\\s*것|마세요|마십시오|말아\\s*주(?:세요|십시오))|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다)(?![가-힣]))|만으로\\s[^.!?\\n]{0,40}?(?<![가-힣])순위를\\s*정하[^.!?\\n]{0,25}?(?:수\\s*없(?:다|습니다|음)(?![가-힣])|(?:해|하)서는\\s*안\\s*(?:됨|된다|됩니다)(?![가-힣]))|(?<![가-힣])순위[^.!?\\n]{0,20}?근거로\\s*(?:[*_]{1,2}\\s*)?사용할\\s*수\\s*(?:는\\s*)?없(?:다|습니다|음)(?![가-힣])|할\\s*수\\s*없는\\s*것\\s{0,3}:\\s{0,3}(?:[*_]{1,2}\\s{0,3})?[^.!?\\n]{0,30}?(?<![가-힣])순위를?\\s*매기거나|(?<![가-힣])표의\\s+(?:나열\\s+)?순서(?:는|가)\\s[^.!?\\n]{0,20}?(?<![가-힣])순위(?:(?:를|을)\\s*뜻하지\\s*(?:[*_]{1,2}\\s*)?않(?:음|는다|습니다)|(?:가|이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣]))",
|
|
888
888
|
"flags": "m",
|
|
889
889
|
"scope": "line",
|
|
890
890
|
"context": [
|
|
891
891
|
"fenced_code",
|
|
892
|
-
"quoted_user_text"
|
|
892
|
+
"quoted_user_text",
|
|
893
|
+
"blockquote_callout"
|
|
893
894
|
],
|
|
894
|
-
"rationale": "A sentence that tells the reader what the document, table or number is not for belongs in the reply or becomes a data fact.
|
|
895
|
+
"rationale": "A sentence that tells the reader what the document, table or number is not for belongs in the reply or becomes a data fact. Shapes: a self-reference topic (이/본/해당/위/아래 + document noun + 은/는) with a negated claim verb, a claim noun + 아님 (순위표 also + 아니고) or 위한/-려는 것이 아님, within one bullet; a comparison or ranking noun + (으)로/처럼 + a negated use or reading verb (순위처럼 읽지 말아 주세요); a purpose-용 data noun (공개용 수치) + 아님; 숫자/수치 + particle said to be incomparable (서로 비교할 수 없다) or compared among themselves toward a quoted verdict (숫자끼리 비교해 \"…빠르다\"는 결론을 내리면 안 됩니다); a speed judgment forbidden for these numbers or a quoted verdict forbidden as a reading (이 숫자로 … 느리다고 판단하면 안 됩니다); ranking refused for this data (…만으로 순위를 정할 수 없다, 순위의 근거로 사용할 수 없다, 할 수 없는 것: 순위를 매기거나); and a table's row order said not to be a ranking (표의 순서는 순위가 아니다). Use verbs after a self-reference, 근거/평가/증거/추천, inference limits (결론 내릴 수 없다, 근거가 되지 않는다), operating rules (…근거로 사용하지 마십시오), conditional method facts (…면 비교할 수 없다, 합계와 비교하면 안 됩니다), type-system rules (숫자 문자열을 그대로 비교하지 마십시오), reported speech and explainers (…면 안 된다는 반론, …면 안 되는 이유), order notices without a table anchor, and legal or financial notices (대체하, 보장하, 권유) stay outside the pattern because operating rules, method lines, manuals, discussions and standard notices use them.",
|
|
895
896
|
"fixture_ids": [
|
|
896
897
|
"ko-negative-pledge:positive",
|
|
897
898
|
"ko-negative-pledge:negative"
|
|
@@ -906,7 +907,8 @@
|
|
|
906
907
|
"scope": "document",
|
|
907
908
|
"context": [
|
|
908
909
|
"fenced_code",
|
|
909
|
-
"quoted_user_text"
|
|
910
|
+
"quoted_user_text",
|
|
911
|
+
"blockquote_callout"
|
|
910
912
|
],
|
|
911
913
|
"rationale": "Two or more sentences that end in 해석하지/주장하지/대표하지 않음 or a claim noun + 이/가 + 아님 turn a document into a list of disclaimers. 의미하, 간주하, 인용하 and the use verbs are left out because repeated definitions, data-processing rules, method lines and operating rules end that way; a single such sentence stays with the warn-only ko-status-negation.",
|
|
912
914
|
"fixture_ids": [
|
|
@@ -918,14 +920,15 @@
|
|
|
918
920
|
"id": "en-negative-pledge",
|
|
919
921
|
"lang": "en",
|
|
920
922
|
"severity": "block",
|
|
921
|
-
"pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
|
|
923
|
+
"pattern": "\\b(?:this|these)\\s+(?:deck|report|document|analysis|table|chart|figure|figures|numbers|results|slide|comparison)\\s+(?:(?:is|are)\\s+not\\s+(?:intended|meant|to\\s+be\\s+(?:read|used|taken|interpreted))(?:\\s+to\\s+be\\s+(?:read|used|taken|interpreted))?\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|(?:should|must)\\s+not\\s+be\\s+(?:read|used|taken|interpreted)\\s+as\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b)|\\b(?:this|these)\\s+(?:is|are)\\s+not\\s+(?:intended|meant)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthe\\s+(?:deck|report|document|analysis)\\s+makes?\\s+no\\s+claims?\\s+(?:about|that|regarding|as\\s+to)\\b|(?:^|\\s[-*]\\s)\\s*(?:[-*]\\s+|\\d+\\.\\s+)?(?:[*_]{1,2})?not\\s+(?:intended|meant|to\\s+be\\s+read|to\\s+be\\s+used)\\s+as\\b(?!\\s*(?:a\\s+substitute|a\\s+replacement|(?:legal|investment|medical|professional)\\s+advice))\\s+(?:an?\\s+|the\\s+)?(?:[A-Za-z0-9-]+\\s+){0,4}?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\b(?:this|these)\\s+(?:table|chart|figure|figures|numbers|results|comparison|analysis)\\s+(?:does|do)\\s+not\\s+constitute\\s+(?:an?\\s+)?[^.!?]{0,60}?\\b(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)|\\bthey\\s+are\\s+not\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|\\b(?:do\\s+not|don[’']t)\\s+compare\\s+(?:these|those)\\s+(?:numbers|figures|results|values|scores|latencies)(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:these|those|the)\\s+(?:numbers|figures)\\s+are\\s+not\\s+comparable(?:\\s+(?:with|to)\\s+(?:each\\s+other|one\\s+another)\\b|(?=\\s*(?:[.!?*_]|$)))|\\b(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+(?:is|are)\\s+not\\s+(?:(?:meant|intended)\\s+)?(?:(?:to\\s+be\\s+)?(?:an?\\s+)?(?:ranking|benchmark|comparison|leaderboard|endorsement)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)(?=\\s*(?:[.!?*_]|$))|to\\s+(?:rank|compare|benchmark)\\b)|\\b(?:do\\s+not|don[’']t)\\s+read\\s+(?:this|the|these|our)\\s+(?:table|chart|figure|graph|list|report|document|deck|slide|page|summary|overview|analysis)s?\\s+as\\s+(?:an?\\s+)?(?:comparison|benchmark|ranking)s?\\b(?!-)(?!\\s+(?:arm|arms|group|groups|standard|standards|class|baseline)\\b)",
|
|
922
924
|
"flags": "i",
|
|
923
925
|
"scope": "sentence",
|
|
924
926
|
"context": [
|
|
925
927
|
"fenced_code",
|
|
926
|
-
"quoted_user_text"
|
|
928
|
+
"quoted_user_text",
|
|
929
|
+
"blockquote_callout"
|
|
927
930
|
],
|
|
928
|
-
"rationale": "A sentence that says the deck, report or numbers are not intended, meant or to be read as a comparison, benchmark or ranking, or make no claim about something belongs in the reply, and so does a table, chart or set of numbers that does not constitute one. Requiring one of those nouns keeps safety labels and advice, offer or diagnosis notices clean; \"does not constitute\" for a report or document stays a notice form, and \"the\" is not a subject except with \"makes no claim\".",
|
|
931
|
+
"rationale": "A sentence that says the deck, report or numbers are not intended, meant or to be read as a comparison, benchmark or ranking, or make no claim about something belongs in the reply, and so does a table, chart or set of numbers that does not constitute one; \"they are not a ranking.\", \"do not compare these numbers (with each other).\" and \"the numbers are not comparable.\" are the same refusal when they close the sentence. Requiring one of those nouns keeps safety labels and advice, offer or diagnosis notices clean; a refusal that continues (a ranking of schools, comparable with the previous release, compare these values directly; …, this is not a benchmark, it is …) describes or instructs and stays clean; a deliverable subject (this table, the report, our deck) that is not a ranking, benchmark, comparison, leaderboard or endorsement, is not meant to rank or compare, or is not to be read as a ranking is the same refusal, while a bare \"this\" is not a subject; \"does not constitute\" for a report or document stays a notice form, and \"the\" is not a subject except with \"makes no claim\" or \"numbers are not comparable\".",
|
|
929
932
|
"fixture_ids": [
|
|
930
933
|
"en-negative-pledge:positive",
|
|
931
934
|
"en-negative-pledge:negative"
|
|
@@ -81,7 +81,12 @@ function sourceRows(text) {
|
|
|
81
81
|
if (/^\s{0,3}>\s?/.test(line)) {
|
|
82
82
|
captionPending = 0;
|
|
83
83
|
figureActive = false;
|
|
84
|
-
|
|
84
|
+
// A quote that opens with a bold label (> **주의:** ...) is the author's own callout rather than
|
|
85
|
+
// quoted text. Only rules that list blockquote_callout read it, with the marker blanked.
|
|
86
|
+
const callout = /^\s{0,3}>\s?(?:\*\*|__)[^*_\n]{1,24}(?:\*\*|__)/.test(line)
|
|
87
|
+
? inlineCodeAsSpaces(line.replace(/^\s{0,3}>\s?/, (prefix) => ' '.repeat(prefix.length)))
|
|
88
|
+
: '';
|
|
89
|
+
return { line, number: index + 1, candidate: '', callout, skipped: true, caption: false, captionAttribution: false };
|
|
85
90
|
}
|
|
86
91
|
const candidate = inlineCodeAsSpaces(line);
|
|
87
92
|
const caption = isCaption(line);
|
|
@@ -148,7 +153,8 @@ function splitSentences(span) {
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
function ruleSegments(rows, rule, cache) {
|
|
151
|
-
const
|
|
156
|
+
const readCallouts = rule.context.includes('blockquote_callout');
|
|
157
|
+
const cacheKey = `${rule.scope}/${rule.context.includes('table_figure_caption')}/${readCallouts}`;
|
|
152
158
|
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
153
159
|
const skipCaptions = rule.context.includes('table_figure_caption');
|
|
154
160
|
const segments = [];
|
|
@@ -161,6 +167,11 @@ function ruleSegments(rows, rule, cache) {
|
|
|
161
167
|
};
|
|
162
168
|
|
|
163
169
|
for (const row of rows) {
|
|
170
|
+
if (readCallouts && row.callout) {
|
|
171
|
+
flush();
|
|
172
|
+
segments.push({ text: row.callout, line: row.number });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
164
175
|
const captionExempt = skipCaptions && (row.caption || row.captionAttribution);
|
|
165
176
|
if (row.skipped || captionExempt || (rule.scope === 'sentence' && !row.candidate.trim())) {
|
|
166
177
|
flush();
|
|
@@ -7,6 +7,18 @@ import { parseRules, scanText } from "./core.mjs";
|
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const MAX_INPUT_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
// The caller reads this output through a bounded pipe. A flood of repeated hits must not grow the
|
|
11
|
+
// output past that bound, because an overflow is read as a scanner failure and the write goes through.
|
|
12
|
+
const MAX_FINDINGS = 64;
|
|
13
|
+
|
|
14
|
+
// Keep every block finding ahead of warnings so a block can never be trimmed away, then restore scan order.
|
|
15
|
+
const boundFindings = (findings) => {
|
|
16
|
+
if (findings.length <= MAX_FINDINGS) return findings;
|
|
17
|
+
const indexed = findings.map((finding, index) => ({ finding, index }));
|
|
18
|
+
const blocks = indexed.filter(({ finding }) => finding.severity === "block");
|
|
19
|
+
const others = indexed.filter(({ finding }) => finding.severity !== "block");
|
|
20
|
+
return [...blocks, ...others].slice(0, MAX_FINDINGS).sort((a, b) => a.index - b.index).map(({ finding }) => finding);
|
|
21
|
+
};
|
|
10
22
|
|
|
11
23
|
try {
|
|
12
24
|
const raw = readFileSync(0);
|
|
@@ -22,7 +34,7 @@ try {
|
|
|
22
34
|
if (totalBytes > MAX_INPUT_BYTES) throw new Error("aggregate content exceeds the bounded scan size");
|
|
23
35
|
findings.push(...scanText(artifact.text, rules, artifact.path));
|
|
24
36
|
}
|
|
25
|
-
process.stdout.write(`${JSON.stringify({ findings })}\n`);
|
|
37
|
+
process.stdout.write(`${JSON.stringify({ findings: boundFindings(findings), total: findings.length })}\n`);
|
|
26
38
|
} catch {
|
|
27
39
|
process.stderr.write("lit-humanizer scanner failed\n");
|
|
28
40
|
process.exitCode = 1;
|