@litfamily/litclaude 1.0.29 → 1.0.30

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.30 - 2026-10-16
4
+
5
+ - 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.
6
+ - 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.
7
+
3
8
  ## 1.0.29 - 2026-10-15
4
9
 
5
10
  - 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.29/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/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>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/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.29/docs/assets/readme/badge-version.svg" alt="1.0.29" />
8
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
7
+ <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" />
8
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/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.29/README_ko-KR.md">한국어</a>
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.30/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.29/docs/privacy.md)
77
- - [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/docs/migration.md)
78
- - [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/CHANGELOG.md)
79
- - [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/LICENSE)
76
+ - [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/privacy.md)
77
+ - [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/migration.md)
78
+ - [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/CHANGELOG.md)
79
+ - [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/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.29/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/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.29/docs/assets/readme/badge-version.svg" alt="1.0.29" />
8
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
7
+ <img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/assets/readme/badge-version.svg" alt="1.0.30" />
8
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/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.29/README.md">English</a>
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.30/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.29/docs/privacy.md)
75
- - [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/docs/migration.md)
76
- - [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/CHANGELOG.md)
77
- - [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.29/LICENSE)
74
+ - [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/privacy.md)
75
+ - [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/docs/migration.md)
76
+ - [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/CHANGELOG.md)
77
+ - [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.30/LICENSE)
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.29"><title>release: 1.0.29</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.29</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.30"><title>release: 1.0.30</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.30</text></g></svg>
package/docs/hooks.md CHANGED
@@ -468,8 +468,12 @@ It shows the settled state, with no entrances or crossfades. While LitClaude wor
468
468
  SMIL animation inside the drawing, which the surface runs in a script-less sandboxed frame: the
469
469
  pill and the border breathe and the step's spinner turns, and on `full` a light crosses the
470
470
  current stage and the current chips, a head runs the rail leaving it, the count bar's head
471
- shimmers and a comet runs around the border. An idle card is a still image. The step's clock
472
- moves with each new drawing, about every two seconds.
471
+ shimmers and a comet runs around the border. An idle card is a still image. The app loads each new
472
+ drawing from scratch and restarts its animation, so this card names the running tool without its
473
+ clock and shows the loop round without its countdown. It is redrawn only when something you would
474
+ notice changes, such as the stage, the tool, a skill, a usage figure or the goal. While the card
475
+ shows in a terminal and the app at once, the terminal's loop countdown and lane times wait for such
476
+ a change too; its step clock keeps counting on its own.
473
477
 
474
478
  Options, in `/config` under LitClaude (stored in `pluginConfigs` in `~/.claude/settings.json`,
475
479
  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.29",
3
+ "version": "1.0.30",
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": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "litclaude",
3
3
  "description": "Claude Code-native workflow plugin with a bounded-authority start-work lifecycle.",
4
- "version": "1.0.29",
4
+ "version": "1.0.30",
5
5
  "author": {
6
6
  "name": "LitClaude contributors"
7
7
  },
@@ -9,7 +9,7 @@ import {
9
9
  } from "../lib/wikify-knowledge.mjs";
10
10
 
11
11
  const protocolVersion = "2024-11-05";
12
- const serverVersion = "1.0.29";
12
+ const serverVersion = "1.0.30";
13
13
 
14
14
  const publicSourceReadTool = {
15
15
  name: "public_source_read",
@@ -50,6 +50,8 @@ const LONG_TURN_MS = 3 * 60 * 1000;
50
50
 
51
51
  const createLive = (options) => ({
52
52
  band: normalizeModSettings(options).band,
53
+ motion: normalizeModSettings(options).motion,
54
+ drawnOn: {},
53
55
  iconFont: { present: false, path: null, note: "basic: not checked yet" },
54
56
  env: {},
55
57
  snapshot: null,
@@ -90,6 +92,28 @@ const stateIo = ($) => ({
90
92
 
91
93
  const toastText = (raised) => `🔥 ${raised.title}${raised.detail ? ` · ${raised.detail}` : ""}`;
92
94
 
95
+ // The model less what moves with time alone: the step clock, the loop countdown, the lanes' run times.
96
+ const stillModel = (model) => model && {
97
+ ...model,
98
+ step: model.step && { name: model.step.name },
99
+ autoloop: model.autoloop && { n: model.autoloop.n, max: model.autoloop.max },
100
+ lanes: { ...model.lanes, list: model.lanes.list.map(({ elapsedMs, ...lane }) => lane) },
101
+ };
102
+
103
+ // What the card would show on each surface still attached. Invalidation redraws every surface at
104
+ // once, and the desktop's Svg is loaded afresh with each new source, restarting its animations; so
105
+ // while a drawing surface is attached, time alone never redraws: it is compared by its drawing and
106
+ // the terminal by the still model (its surface module counts the step clock on by itself). A
107
+ // terminal on its own is compared by the whole model, so its countdown and lane times move too.
108
+ function drawnKey(live, model) {
109
+ const kinds = new Set(Object.values(live.drawnOn));
110
+ if (kinds.size === 0) return JSON.stringify(model);
111
+ const withSvg = kinds.has("svg");
112
+ return [...kinds].map((kind) => (kind === "svg"
113
+ ? (model?.visible ? composeSvgCard(model, { motion: live.motion, accent: live.accent }).source : "hidden")
114
+ : JSON.stringify(withSvg ? stillModel(model) : model))).join("\n");
115
+ }
116
+
93
117
  async function rebuild($, live) {
94
118
  const now = await $.clock.now();
95
119
  const previous = live.model;
@@ -117,7 +141,7 @@ async function rebuild($, live) {
117
141
  $.ui.invalidate("ui.render");
118
142
  });
119
143
  }
120
- if (JSON.stringify(previous) !== JSON.stringify(next)) $.ui.invalidate("ui.render");
144
+ if (drawnKey(live, previous) !== drawnKey(live, next)) $.ui.invalidate("ui.render");
121
145
  return next;
122
146
  }
123
147
 
@@ -328,6 +352,14 @@ export const register = (on, options) => {
328
352
  return next(e);
329
353
  }).catch(($, e, next) => next(e));
330
354
 
355
+ // A client left: once no client of its surface remains, that surface stops counting.
356
+ on("session.detach", async ($, e, next) => {
357
+ const result = await next(e);
358
+ const roster = await $.session.surfaces();
359
+ if (!roster.includes(e.surface)) delete live.drawnOn[e.surface];
360
+ return result;
361
+ }).catch(($, e, next) => next(e));
362
+
331
363
  on("ui.fault", async ($, e, next) => {
332
364
  if (e.component === "AbovePrompt") live.clientFailed = true;
333
365
  return next(e);
@@ -341,6 +373,7 @@ export const register = (on, options) => {
341
373
  const elements = $.ui.resolve(e);
342
374
  const columns = Math.max(20, e.props.bodyColumns ?? e.viewport?.columns ?? 80);
343
375
  let drawn = null;
376
+ live.drawnOn[e.surface] = e.surface !== "terminal" && elements.Svg ? "svg" : "cells";
344
377
  if (e.surface !== "terminal" && elements.Svg) {
345
378
  // A desktop or editor draws Text in a proportional font, so the cell-built card comes apart
346
379
  // there; those surfaces get the card as one drawing instead.
@@ -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", "9018c5793e0c9e45e416a1d9db453f2e18283ababba7f87a92ff288ec442864d"],
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", "673864584ac4ca8d054be6adb8460a45e19c950456cd26ba7b2f41923f40ac6b"],
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", "c69c11a0e9315b5dfb8c339e3c97522593cd7452955b187526406b29d1f16671"],
621
- ["skills/lit-humanizer/rules.json", "6153ca0835b04fc9512c903b4c5080c3a45cd1fd68c1a9dbad8972b5a150fb6c"],
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", "748b008b02d1fab1be4b7784cc7b195f89a3b1a992b8975b6040fc30fc16c904"],
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", "2743185b72ab3ef5d94b389f0f6aba12d3beb945e717e06090e4aeaffdd00ac9"],
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
- return { findings: parsed.findings };
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 omitted = findings.length > 8 ? `; ${findings.length - 8} more` : "";
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
 
@@ -11,15 +11,15 @@
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's clock moves with each new drawing the mod hands the surface.
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
24
  const C = Object.freeze({
25
25
  bg: "#0d1117",
@@ -113,7 +113,7 @@ 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 }], [` · ${formatDuration(model.autoloop.leftMs)}`, { fill: C.dim }]]);
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 }]]);
@@ -220,7 +220,7 @@ const mapRowSvg = (model, accent, animate) => {
220
220
 
221
221
  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, tool and clock on the right.
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 clock = ` · ${formatClock(model.step.elapsedMs)}`;
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">`
@@ -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} ${formatClock(model.step.elapsedMs)}`);
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 three shapes only: a self-reference topic (이/본/해당/위/아래 plus a document noun) with a negated claim verb or claim noun, a comparison or ranking noun with a negated use verb, and a purpose-용 data noun with 아님. 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 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.
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*것|마세요|마십시오)|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다))|(?:(?:비교|결론|벤치마크|순위|대표값|일반화)(?:이|가)|목적이|위한\\s*것이)\\s*(?:[*_]{1,2}\\s*)?(?:아님|아니다|아닙니다)(?![가-힣]))|(?:비교|결론|벤치마크|순위|대표값|일반화)(?:으로|로)\\s*(?:[*_]{1,2}\\s*)?(?:쓰|사용하|해석하|읽|간주하|인용하)(?:지\\s*(?:[*_]{1,2}\\s*)?(?:않(?:음|는다|습니다)|말\\s*것|마세요|마십시오)|(?:으)?면\\s*(?:[*_]{1,2}\\s*)?안\\s*(?:됨|된다|됩니다))|(?:공개|배포|대외|외부|홍보|발표|보고|비교|인용|공식)용\\s*(?:자료|문서|수치|표|그래프)(?:이|가)\\s*(?:[*_]{1,2}\\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. Three shapes: a self-reference topic (이/본/해당/위/아래 + document noun + 은/는) with a negated claim verb or a claim noun + 아님 within one bullet; a comparison or ranking noun + (으)로 + a negated use verb; a purpose-용 data noun (공개용 수치) + 아님. Use verbs after a self-reference, 근거/평가/증거/추천, and legal or financial notices (대체하, 보장하, 권유) stay outside the pattern because operating rules, method lines and standard notices use them.",
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
- return { line, number: index + 1, candidate: '', skipped: true, caption: false, captionAttribution: false };
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 cacheKey = `${rule.scope}/${rule.context.includes('table_figure_caption')}`;
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;