asciidoc-pubkit 0.6.1 → 0.6.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c946ae67aae9ea8e6e21f5464b61daec42a18488c5aca5fbe434921f13dad824
4
- data.tar.gz: 47ca9c1414c78699dbb07ebe3bfdb07460cc132a8a10c642392d147eb2247b90
3
+ metadata.gz: 7ce6fe84fae75593d9444c1db5d7abb1f6c96823b77376f90750d9554358af76
4
+ data.tar.gz: 924c12f91b714b34c1883c5a0583259d2c97989a328d412e6648d681de01984c
5
5
  SHA512:
6
- metadata.gz: f8e1382976343a206f04363bdfb0896e3633605f239c2fbfb46afacaaf11415a93757a418c09d9fc449700742b15268f69883a36e0c7e200f9f3e73b4f4a0f22
7
- data.tar.gz: 3f0395078d442df25b730cde44186cb1868c3b68f7ea292438a2ad8871179a30198524fcd846c5ea6f2ce84d65d90f01b9d4db829f012b6e47e68f4b2051d3da
6
+ metadata.gz: b194a8c6af0423b90c41275a1287bf1bdf0b6edee930a27b99bf9cddcce15d8f348e4ea58b3008e2f0f2ef533c9b4a4d43cd74262d3cd01c00b051e96aead0ba
7
+ data.tar.gz: ea6b5543a41d7f7d4780b3626f1e33797c55198d428f413295e9576894fc30e92b6ca890b7461382ac069612b7a48027250bcd8c6ef937bc59fd205bc5f58be6
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.2 — 2026-10-01
6
+
7
+ - Explain review and scoring architecture, flows, and algorithms in a separate
8
+ README section with Mermaid diagrams. Add concrete applications for each
9
+ editorial and research reference.
10
+
11
+ - Locate `repeated-ending` candidates at the matching ending in the third
12
+ consecutive sentence, preserving Unicode columns and line offsets. Do not
13
+ join repetitions across a sentence without a matching ending.
14
+ - Extend four configured metaphorical-operation phrases with MeCab/IPADIC
15
+ predicate matching, including past, negative, and auxiliary forms. Retain
16
+ polarity, custom-rule replacement, allow lists, protected text, and longest
17
+ phrase precedence; literal mode keeps exact phrase matching.
18
+
5
19
  ## 0.6.1 — 2026-10-01
6
20
 
7
21
  - Add `review score FILE` for direct AsciiDoc manuscript scoring without a
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  A toolkit for authoring and reviewing AsciiDoc books.
9
9
 
10
- Version 0.6.1 provides shared Japanese technical writing criteria, an authoring
10
+ Version 0.6.2 provides shared Japanese technical writing criteria, an authoring
11
11
  prompt, contextual review prompts, manuscript scoring, and baseline verification
12
12
  for edited manuscripts.
13
13
  The CLI, diagnostics, documentation, and generated instructions are in English.
@@ -23,7 +23,7 @@ Starting with version 0.1.1, the default review backend requires the external Me
23
23
  command and a UTF-8 IPADIC dictionary. Node.js and textlint are not required.
24
24
  Explicit `--tokenizer literal` mode provides limited phrase matching without MeCab.
25
25
 
26
- Version 0.6.1 also provides `review score FILE` for a manuscript score.
26
+ Version 0.6.2 also provides `review score FILE` for a manuscript score.
27
27
  It uses local candidate analysis by default; `--agent codex` or `--agent claude`
28
28
  explicitly asks an installed CLI to evaluate readability and may connect to its
29
29
  configured model provider. No scoring mode edits manuscripts.
@@ -80,7 +80,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
80
80
 
81
81
  ```ruby
82
82
  source 'https://rubygems.org'
83
- gem 'asciidoc-pubkit', '~> 0.6.1'
83
+ gem 'asciidoc-pubkit', '~> 0.6.2'
84
84
  ```
85
85
 
86
86
  Then install dependencies and run the CLI through Bundler:
@@ -102,7 +102,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
102
102
  cd asciidoc-pubkit
103
103
  bundle install
104
104
  gem build asciidoc-pubkit.gemspec
105
- gem install ./asciidoc-pubkit-0.6.1.gem
105
+ gem install ./asciidoc-pubkit-0.6.2.gem
106
106
  asciidoc-pubkit --version
107
107
  ```
108
108
 
@@ -126,7 +126,7 @@ asciidoc-pubkit writing prompt --lang ja --output writing-prompt.md
126
126
  ```
127
127
 
128
128
  For example, `--lang en` exits with an unsupported-language error. No English
129
- review or writing criteria are shipped in 0.6.1.
129
+ review or writing criteria are shipped in 0.6.2.
130
130
 
131
131
  For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
132
132
  paragraphs deliberately contain review candidates; its code block must remain
@@ -144,29 +144,13 @@ asciidoc-pubkit review score book.adoc --agent codex
144
144
  asciidoc-pubkit review score book.adoc --agent claude --json
145
145
  ```
146
146
 
147
- Without `--agent`, `candidate-density-v1` reports a mechanical review indicator:
148
- `max(0, 100 - 5 * candidates * 1000 / characters)`, rounded to three decimals.
149
- Characters are non-whitespace Unicode characters in source-mapped running prose
150
- after inline masking. Every finding, including informational candidates, counts
151
- once. Lower scores indicate more detected review cues per 1,000 characters, not
152
- proven defects or AI authorship. Short texts are especially sensitive to a single
153
- candidate. No unmasked prose yields `null` rather than a perfect score.
154
- Successful scoring exits 0 regardless of the score; invalid input, configuration,
155
- analyzer, evaluator, or output failures exit 2. Verification exit codes are
156
- unchanged. The JSON report includes candidate counts, findings, coverage notices,
157
- analysis identity, and the formula. Rule, allow-list, and tokenizer changes can
158
- change the score even when the manuscript is unchanged.
159
-
160
- With `--agent`, `llm-readability-v1` displays an evaluator score instead. Sentence
161
- clarity and paragraph coherence are each rated 1–3 per paragraph and mapped to
162
- 0, 50, and 100. The displayed score averages these two dimensions across scored
163
- paragraphs with equal paragraph weights. JSON also retains the individual
164
- ratings, English reasons, source locations, evaluator model and CLI version,
165
- rubric fingerprint, and the mechanical candidate-density score. These ordinal
166
- averages are descriptive summaries, not validated interval measurements. This
167
- single-document evaluation has no reference manuscript or evidence checklist;
168
- it does not measure fact retention, technical accuracy, or meaning preservation.
169
- `meaning_verified` remains `false` in both modes.
147
+ Without `--agent`, the report shows a mechanical candidate-density indicator.
148
+ With `--agent`, it shows a model readability rating and retains the mechanical
149
+ score in JSON. See [How review and scoring work](#how-review-and-scoring-work)
150
+ for the analysis pipeline, formulas, and interpretation of these different scores.
151
+ Successful scoring exits 0 regardless of the score; input, configuration,
152
+ analyzer, evaluator, or output failures exit 2. No scoring mode establishes
153
+ technical accuracy or meaning preservation; `meaning_verified` remains `false`.
170
154
 
171
155
  Scoring uses the same configuration discovery, custom rules, glossary, allows,
172
156
  style, language, base directory, include handling, and prose coverage as scanning.
@@ -368,9 +352,192 @@ or release readiness: `meaning_verified` is always `false`.
368
352
  | `1` | Verification found protected-content, structure, source, or parsing issues |
369
353
  | `2` | Invalid arguments, configuration, session, scan input, or output failure |
370
354
 
355
+ ## How review and scoring work
356
+
357
+ This section explains the implementation independently of command syntax.
358
+ [Review workflow](#review-workflow) covers commands and session handling;
359
+ [Score an AsciiDoc manuscript](#score-an-asciidoc-manuscript) covers scoring options.
360
+ The diagrams use GitHub-supported Mermaid fenced blocks.
361
+
362
+ ### Shared components and prose selection
363
+
364
+ Both paths resolve the same settings and parse the AsciiDoc entry file with
365
+ Asciidoctor, including local includes. `Document` selects running-prose paragraphs
366
+ and maps them back to their source files. Ambiguous mappings become coverage
367
+ notices; the analyzer does not guess locations. Headings provide context, while
368
+ code, tables, quotations, lists, and protected blocks are excluded from prose
369
+ analysis. Inline code, quoted spans, links, and other protected constructs are
370
+ masked with spaces that preserve character offsets and newlines.
371
+
372
+ ```mermaid
373
+ flowchart TD
374
+ A["AsciiDoc entry and local includes"] --> D["Document: parse and map running prose"]
375
+ C["Settings: options, configuration, resolved rules"] --> D
376
+ D --> P["Paragraphs with source locations and headings"]
377
+ D --> N["Coverage notices and parser diagnostics"]
378
+ P --> M["Rules: mask protected inline content"]
379
+ C --> M
380
+ M --> F["Candidate detection"]
381
+ F --> R["Session: review baseline and evidence"]
382
+ F --> S["Score: manuscript indicators"]
383
+ P -->|"Original prose and headings"| S
384
+ N --> R
385
+ N --> S
386
+ ```
387
+
388
+ `Settings` resolves rule precedence as command-line `--rules`, configuration
389
+ `review.rules`, then packaged defaults. A custom rule file replaces the whole
390
+ set. `Rules` returns review candidates with one-based Unicode line and column
391
+ positions; a match does not establish a defect. `Session` saves evidence for a
392
+ later review, while `Score` returns a report directly without creating a session.
393
+ Parser diagnostics must be resolved before scanning or scoring.
394
+
395
+ ### Review: collect, judge, and verify
396
+
397
+ Scanning freezes the source baseline, resolved settings and rules, analyzer
398
+ identity, and writing criteria. The session consists of `manifest.json`,
399
+ `document.json`, `findings.json`, and `baseline/`. Prompt generation checks
400
+ artifact integrity and rejects sources changed since the scan. It emits one
401
+ Markdown file containing the saved criteria, context, every selected paragraph
402
+ in document order, and its candidate evidence, including paragraphs with no
403
+ candidates. Generating that prompt requires no analyzer or model invocation.
404
+
405
+ ```mermaid
406
+ sequenceDiagram
407
+ actor Reviewer as Human or external agent
408
+ participant CLI as pubkit CLI
409
+ participant Sources as Manuscript files
410
+ participant Session as Saved review session
411
+ Reviewer->>CLI: review scan FILE
412
+ CLI->>Sources: Parse prose and detect candidates
413
+ CLI->>Session: Save evidence and source baseline
414
+ Reviewer->>CLI: review prompt SESSION
415
+ CLI->>Session: Check integrity and read saved evidence
416
+ CLI->>Sources: Check sources still match scan
417
+ CLI-->>Reviewer: One Markdown review prompt
418
+ Reviewer->>Sources: Judge context and edit prose externally
419
+ Reviewer->>CLI: review verify SESSION
420
+ CLI->>Session: Read original baseline and settings
421
+ CLI->>Sources: Reparse and compare protected content
422
+ CLI-->>Reviewer: Mechanical result, notices, remaining candidates
423
+ ```
424
+
425
+ The reviewer decides whether to keep or revise each paragraph and checks source
426
+ evidence before adding claims. `scan`, `prompt`, and `verify` never invoke an AI
427
+ CLI or edit manuscripts. Verification compares the included source set, content
428
+ outside reviewed prose, protected inline tokens, document structure, and analyzer
429
+ identity against the saved baseline. Numeric changes produce manual-review
430
+ notices; remaining candidates alone do not fail verification. A passing result
431
+ means these mechanical checks passed, with `meaning_verified: false`.
432
+
433
+ ### Detection: contextual rules and token boundaries
434
+
435
+ MeCab with UTF-8 IPADIC is the default analyzer. It supplies surfaces, dictionary
436
+ forms, part of speech, and source-aligned token spans. Explicit literal mode
437
+ matches configured surface strings; there is no silent fallback. Both modes
438
+ apply glossary, configured prose style, and sentence-ending checks.
439
+
440
+ ```mermaid
441
+ flowchart TD
442
+ P["Masked running prose"] --> B{"Selected tokenizer"}
443
+ B -->|"mecab, default"| T["MeCab and IPADIC tokens with source offsets"]
444
+ T --> L["Lemma rules with allow lists and contextual overlap checks"]
445
+ T --> X["Four enabled contextual predicate patterns"]
446
+ B -->|"literal, explicit"| E["Exact configured terms"]
447
+ P --> Q["Exact framing and contextual phrases"]
448
+ L --> F["Candidate findings"]
449
+ X --> O["Prefer most specific phrase; apply allow lists"]
450
+ E --> O
451
+ Q --> O
452
+ O --> F
453
+ P --> G["Glossary, style, and repeated endings"]
454
+ G --> F
455
+ ```
456
+
457
+ Morphological rules match configured noun and adjective lemmas, canonical verb
458
+ forms, adjacent compound nouns, and sahen nouns followed by `する` or `できる`.
459
+ Predicate spans extend through adjacent auxiliaries, dependent verbs, and
460
+ supported connective particles. Negative auxiliaries preserve polarity;
461
+ negative-only rules require that evidence. Unknown tokens are not guessed.
462
+
463
+ The four contextual patterns combine an exact prefix (`地味に`, `静かに`,
464
+ `時間を`, or `側に`) with a known independent verb lemma (`効く`, `壊れる`,
465
+ `溶かす`, or `倒す`). The canonical phrase must be enabled in the resolved rules.
466
+ For example, `地味に効かなかった` is collected as one complete candidate with
467
+ lemma `地味に効く` and `negative: true`. Protected text and paragraph boundaries
468
+ cannot bridge the pattern. Longer contextual phrases suppress contained matches,
469
+ including when the longer phrase is allowed. These limited patterns suggest
470
+ reviewing the actual effect or operation; they do not prove a metaphor is wrong.
471
+
472
+ Sentence-ending analysis splits each paragraph at `。`, `!`, and `?`, retaining
473
+ source offsets. It extracts supported endings such as `検証します` and `です`,
474
+ then checks consecutive windows of three sentences. A sentence without a matching
475
+ ending breaks the run. The first matching run produces one informational
476
+ candidate at the ending of its third sentence. Precise technical repetition may
477
+ be worth retaining; this is not a requirement to vary verbs.
478
+
479
+ ### Scoring: density and optional readability judgment
480
+
481
+ Scoring first runs the shared candidate detector. For the default
482
+ `candidate-density-v1`, let `N` be all findings, including informational ones,
483
+ and `C` be non-whitespace Unicode characters after inline masking:
484
+
485
+ ```text
486
+ density = N * 1000 / C
487
+ score = max(0, 100 - 5 * density)
488
+ ```
489
+
490
+ The score is rounded to three decimals. No unmasked prose yields `null`.
491
+ More candidates per 1,000 characters lower the score; short texts are particularly
492
+ sensitive. Changing rules, allow lists, or the detector can change the score of
493
+ an unchanged manuscript. This heuristic is a review indicator, not a calibrated
494
+ quality measure or a probability that AI wrote the text.
495
+
496
+ ```mermaid
497
+ flowchart TD
498
+ P["Selected prose and detected candidates"] --> D["Compute candidate density and mechanical score"]
499
+ D --> A{"Explicit agent option and nonempty prose?"}
500
+ A -->|"No"| R["Text or JSON report"]
501
+ A -->|"Yes"| I["Paragraph IDs, text, headings, and readability rubric"]
502
+ I --> J["Installed Codex or Claude CLI; configured model provider"]
503
+ J --> V["Validate IDs, integer ratings, and reasons"]
504
+ V --> M["Map ratings to 0, 50, 100 and average"]
505
+ M --> R
506
+ D --> K["Retain mechanical score in JSON"]
507
+ K --> R
508
+ ```
509
+
510
+ With explicit `--agent`, `llm-readability-v1` asks a fresh local CLI invocation to
511
+ rate each paragraph's sentence clarity and paragraph coherence separately from
512
+ 1 to 3. The rubric targets engineers familiar with the technical terms. The
513
+ input contains paragraph IDs, original text, and headings, without candidate
514
+ findings or file paths. The CLI may connect to its configured model provider.
515
+ The response must contain every supplied ID exactly once, integer ratings in
516
+ range, and nonempty reasons. Invalid responses fail scoring.
517
+
518
+ Ratings of 1, 2, and 3 map to 0, 50, and 100. Each dimension averages equally
519
+ across scored paragraphs; the displayed score averages the two dimension means.
520
+ JSON retains both dimensions, paragraph reasons, evaluator identity, and the
521
+ mechanical score. These ordinal averages summarize a fallible judgment. There
522
+ is no reference manuscript or fact checklist in this single-document path, so
523
+ it does not assess preservation or establish technical accuracy. Temporary CLI
524
+ artifacts are removed after the invocation, and manuscripts remain unchanged.
525
+
526
+ | Result | What it measures | Evidence needed |
527
+ | --- | --- | --- |
528
+ | Candidate-density score | Detected cues per text length | Current prose and resolved rules |
529
+ | Optional readability score | Model judgment of clarity and coherence | Current prose, context, and rubric |
530
+ | Development benchmark accuracy | Detector agreement and location accuracy | Fixed corpus with annotated targets |
531
+ | Review verification | Mechanical preservation after editing | Original session baseline and current files |
532
+
533
+ See the [development prose benchmark](#development-prose-benchmark) for
534
+ before/after trials with gold targets, separate fact checklists, and evaluator
535
+ calibration. Its precision, recall, and F1 evaluate the detector rather than
536
+ providing a score for an arbitrary manuscript.
537
+
371
538
  ## Configuration
372
539
 
373
- `scan` searches upward from the entrypoint directory for the nearest
540
+ `scan` and `score` search upward from the entrypoint directory for the nearest
374
541
  `.asciidoc-pubkit.yml`. Use `--config FILE` to select a different file. CLI options
375
542
  override configuration values; unspecified values use built-in defaults.
376
543
  The initial release loads one configuration file, not merged book/repository files.
@@ -434,8 +601,9 @@ sahen predicate takes precedence over a matching standalone noun rule. Auxiliary
434
601
  negation, past tense, passive forms, and progressive forms in the reported surface.
435
602
  The added reach predicate is negative-only; the existing handling predicate is
436
603
  reviewed in both affirmative and negative forms. Glossary variants and generic
437
- framing phrases continue to use literal matching. Contextual phrases also use
438
- literal matching and suppress overlapping morphological candidates. Compound
604
+ framing phrases continue to use literal matching. Contextual phrases suppress
605
+ overlapping morphological candidates. Except for the four predicate patterns
606
+ described below, they use literal matching. Compound
439
607
  nouns are matched across adjacent noun tokens. Selection and narrowing verbs
440
608
  include potential forms; predicate surfaces also preserve causative auxiliaries.
441
609
 
@@ -460,11 +628,30 @@ not added as general metaphor candidates. Identify the actual effect, policy,
460
628
  work, or failure state from evidence rather than applying a fixed replacement.
461
629
  Keep valid technical meanings and necessary negation.
462
630
 
631
+ Version 0.6.2 additionally matches these four phrases with MeCab/IPADIC
632
+ verb lemmas and adjacent predicate auxiliaries. For example, `地味に効かなかった`,
633
+ `静かに壊れていた`, `時間を溶かしてしまった`, and `側に倒しました` retain their
634
+ complete surfaces and polarity. A pattern is enabled only when its canonical
635
+ phrase (`地味に効く`, `静かに壊れる`, `時間を溶かす`, or `側に倒す`) is present in
636
+ the resolved `contextual-phrase.terms`. Removing those canonical entries from
637
+ custom rules disables their morphological patterns; remaining exact surfaces
638
+ still work. No YAML schema change is required. Allow lists accept the canonical
639
+ phrase or exact detected surface. The most specific overlapping phrase wins,
640
+ including allowed phrases. Masked inline content, unknown tokens, and paragraph
641
+ boundaries cannot bridge a pattern. This remains a limited contextual heuristic,
642
+ not a syntactic or semantic determination of metaphor. Literal mode retains
643
+ exact surfaces and does not acquire this inflection coverage.
644
+
645
+ In version 0.6.2, a `repeated-ending` candidate points to the ending
646
+ of the third sentence in the first consecutive run within each paragraph.
647
+ A sentence without a matching ending interrupts the run. The candidate remains
648
+ informational because precise technical repetition can be necessary.
649
+
463
650
  The shared criteria distinguish reader instructions, actual system behavior,
464
651
  and available capabilities. They also separate readability from checks for
465
652
  missing information and unsupported additions. Sentence length, punctuation,
466
653
  and list density are contextual cues, not fixed acceptance thresholds. These
467
- updates are included in 0.6.1; start a new session to use updated criteria and rules.
654
+ updates are included in 0.6.2; start a new session to use updated criteria and rules.
468
655
 
469
656
  Literal term matching suppresses matches strictly contained in a longer matched
470
657
  term, across categories. The longer term also suppresses contained matches when
@@ -484,12 +671,12 @@ reports analyzer changes instead of treating results from different dictionaries
484
671
  as directly comparable. Prompt generation uses saved evidence and does not need
485
672
  MeCab. Changed rules or dictionary settings require a new scan.
486
673
 
487
- Sessions from earlier tool versions are not compatible with 0.6.1. Keep the original baseline for
674
+ Sessions from earlier tool versions are not compatible with 0.6.2. Keep the original baseline for
488
675
  an ongoing review and finish it with the original version, or start a new review
489
676
  pass in a different directory:
490
677
 
491
678
  ```sh
492
- asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.6.1
679
+ asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.6.2
493
680
  ```
494
681
 
495
682
  Severity describes review priority, not proof of an error. There is no AI-authorship
@@ -568,13 +755,19 @@ Zero denominators are reported as `null`, not perfect scores. Duplicate detectio
568
755
  count as false positives. These scores measure the detector, not the manuscript.
569
756
 
570
757
  The checked-in [`MeCab baseline`](benchmark/baseline-mecab.json), captured before
571
- the planned detector changes, has precision 100, recall 81.818, F1 90, and location
758
+ the detector changes, has precision 100, recall 81.818, F1 90, and location
572
759
  accuracy 77.778. It misses two inflected metaphor phrases and mislocates two
573
760
  repeated-ending candidates. Reports record source, criteria, rules, corpus, and
574
761
  dictionary fingerprints. Comparisons reject changed corpora, scoring versions,
575
762
  analyzers, or dictionaries. Reports with configuration fingerprints also reject
576
763
  changed benchmark configuration. A fixed empty configuration
577
764
  keeps these trials independent of auto-discovered manuscript settings.
765
+ The 0.6.2 location correction raises location accuracy to 100 without
766
+ changing precision, recall, or F1 on this corpus. Adding the four MeCab predicate
767
+ patterns then raises precision, recall, F1, and location accuracy to 100 on these
768
+ 11 targets. This is regression evidence for the fixed synthetic corpus, not
769
+ evidence of general performance on unseen manuscripts; literal recall remains
770
+ 81.818.
578
771
  Record your own baseline when dictionary fingerprints
579
772
  differ. `--tokenizer literal` explicitly selects a separate limited-mode run;
580
773
  there is no automatic fallback, and literal results do not validate MeCab.
@@ -694,7 +887,9 @@ In literal mode, all category term lists use exact phrase matching. In MeCab
694
887
  mode, abstract nouns and degree adjectives use dictionary forms, while predicates
695
888
  use `verbs` and `sahen`; add a predicate's desired literal surface to
696
889
  `weak-predicate.terms` as well if literal mode should detect it. Contextual and
697
- generic framing phrases use literal matching in both modes. Questions apply to
890
+ generic framing phrases use literal matching in both modes, with the
891
+ MeCab exception for the four configured contextual predicate patterns described
892
+ above. Questions apply to
698
893
  both detectors. Glossary, style, repeated-ending checks, inline exclusions, and
699
894
  morphological suffix handling remain implemented in Ruby.
700
895
 
@@ -712,19 +907,23 @@ conclusions. Candidate detection therefore leaves contextual judgment, document
712
907
  structure, and final edits to the reviewer.
713
908
 
714
909
  - [natural-japanese](https://github.com/coji/natural-japanese): separates mechanical
715
- detection from contextual judgment and identifies repetitive framing.
910
+ detection from contextual judgment and identifies repetitive framing. Applied
911
+ here: candidate findings feed a contextual review prompt, while phrases such as
912
+ `重要なのは` and `このように` invite inspection rather than automatic deletion.
716
913
  - [yomiyasu](https://github.com/nanaism/yomiyasu):
717
914
  informs evidence-based review of
718
915
  metaphorical operations, distinctions between instructions and capabilities,
719
- and separate assessment of meaning preservation and readability. The criteria
720
- and regression examples here are independently written; the upstream skill,
721
- Python linter, and benchmark passages are not bundled. Fixed style thresholds
722
- and an AI-likeness score are not adopted.
916
+ and separate assessment of meaning preservation and readability. Applied here:
917
+ four limited metaphor patterns and third-sentence repetition locations inform
918
+ candidate detection; the shared criteria require evidence for concrete rewrites.
723
919
  - [日本語技術文書の文章規範](https://gist.github.com/k16shikano/fd287c3133457c4fd8f5601d34aa817d):
724
- informs paragraph logic, evidence scope, and meaningful uncertainty.
920
+ informs paragraph logic, evidence scope, and meaningful uncertainty. Applied
921
+ here: the shared criteria start from each paragraph's technical purpose and
922
+ preserve necessary conditions, uncertainty, and exact terminology.
725
923
  - [AI臭い文章とは何なのか](https://speakerdeck.com/nasuvitz/ai-kusai-bunshou-toha-nanina-no-ka):
726
924
  provides examples of unnecessary contrast, abstract referents, and paired short
727
- sentences.
925
+ sentences. Applied here: the shared criteria ask reviewers to inspect these
926
+ patterns in context and recover concrete referents and connected explanations.
728
927
 
729
928
  The development evaluation adapts ideas from the following primary research;
730
929
  its engineering audience, corpus, and rubric do not reproduce those benchmarks.
@@ -734,13 +933,18 @@ Research texts and datasets are not redistributed here.
734
933
  (Yamashita et al., LREC 2026): separates information preservation from sentence
735
934
  and document simplicity and evaluates LLM judgments against human annotations.
736
935
  Its elementary-school audience and Wikipedia corpus differ from this project.
936
+ Applied here: the scoring rubric separates sentence clarity from paragraph
937
+ coherence, and the development judge evaluates fact retention independently.
737
938
  - [Evaluating Factuality in Text Simplification](https://aclanthology.org/2022.acl-long.506/)
738
939
  (Devaraj et al., ACL 2022): distinguishes insertion, deletion, and substitution
739
- errors, motivating separate checks for unsupported additions and lost facts.
940
+ errors. Applied here: development trials check each source fact and report
941
+ unsupported additions and substitutions separately; a readable rewrite can
942
+ still fail preservation review.
740
943
  - [Evaluating Document Simplification: On the Importance of Separately Assessing Simplicity and Meaning Preservation](https://aclanthology.org/2024.readi-1.1/)
741
944
  (Cripwell et al., READI 2024): motivates separate readability and preservation
742
- scores rather than a single aggregate. SARI-style overlap metrics are not
743
- adopted because lexical agreement alone is insufficient for this prose review.
945
+ scores. Applied here: readability dimensions and preservation outcomes remain
946
+ separate in benchmark reports; the direct manuscript score makes no
947
+ preservation claim. SARI-style overlap metrics are not adopted.
744
948
 
745
949
  ## License
746
950
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module AsciidocPubkit
4
4
  class Rules
5
+ CONTEXTUAL_PREDICATES = { '地味に' => '効く', '静かに' => '壊れる',
6
+ '時間を' => '溶かす', '側に' => '倒す' }.freeze
5
7
  INLINE = /`[^`\n]*`|\+\+\+.*?\+\+\+|\+\+[^\n]*?\+\+|(?<!\w)\+[^+\n]+\+|「[^」\n]*」|『[^』\n]*』|\{[^}\n]+\}|<<[^>\n]+>>|\[\[[^\]\n]+\]\]|(?:link|xref|image|footnote|pass):[^\s\[]*\[[^\]\n]*\]|https?:\/\/[^\s\[\]<>]+(?:\[[^\]\n]*\])?/m
6
8
  def self.mask(text)
7
9
  # Keep character offsets stable while excluding common inline constructs.
@@ -18,18 +20,24 @@ module AsciidocPubkit
18
20
  token_groups = tokenizer ? tokenize_paragraphs(paragraphs, tokenizer) : []
19
21
  paragraphs.each_with_index do |paragraph, index|
20
22
  text = mask(paragraph.fetch('text'))
21
- scan_morphemes(findings, paragraph, text, token_groups[index], settings, rules, terms) if tokenizer
22
- scan_literal_terms(findings, paragraph, text, terms, settings, morphological: !!tokenizer)
23
+ contextual = tokenizer ? contextual_predicates(text, token_groups[index], terms) : []
24
+ scan_morphemes(findings, paragraph, text, token_groups[index], settings, rules, terms, contextual) if tokenizer
25
+ scan_literal_terms(findings, paragraph, text, terms, settings, morphological: !!tokenizer, contextual: contextual)
23
26
  settings.fetch('glossary').each do |canonical, variants|
24
27
  variants.each do |variant|
25
28
  find_term(findings, paragraph, text, variant, 'glossary-variant', 'warning', "Use #{canonical.inspect} when this variant refers to the same concept; preserve identifiers and quotations.")
26
29
  end
27
30
  end
28
- sentences = text.split(/[。!?]/).map(&:strip).reject(&:empty?)
29
- endings = sentences.map { |sentence| sentence[/(?:\p{Han}+(?:します|できます)|しています|されます|ません|です)\z/] }
30
- repeated = endings.each_cons(3).find { |items| items.first && items.uniq.one? }
31
+ endings = text.to_enum(:scan, /[^。!?]+/).filter_map do
32
+ sentence = Regexp.last_match
33
+ next if sentence[0].strip.empty?
34
+ ending = sentence[0].match(/(\p{Han}+(?:します|できます)|しています|されます|ません|です)[[:space:]]*\z/)
35
+ { 'match' => ending && ending[1], 'offset' => ending && sentence.begin(0) + ending.begin(1) }
36
+ end
37
+ repeated = endings.each_cons(3).find { |items| items.first['match'] && items.map { |item| item['match'] }.uniq.length == 1 }
31
38
  if repeated
32
- add(findings, paragraph, text, text.index(repeated.first), repeated.first, 'repeated-ending', 'info', 'Three consecutive sentences share an ending. Check rhythm without changing precise technical verbs.')
39
+ ending = repeated.last
40
+ add(findings, paragraph, text, ending['offset'], ending['match'], 'repeated-ending', 'info', 'Three consecutive sentences share an ending. Check rhythm without changing precise technical verbs.')
33
41
  end
34
42
  pattern = case settings.fetch('style')
35
43
  when 'desu-masu' then /(?:である|だった|なのだ)(?=。|\z)/
@@ -68,13 +76,40 @@ module AsciidocPubkit
68
76
  end
69
77
  end
70
78
 
71
- def self.scan_morphemes(findings, paragraph, text, tokens, settings, rules, terms)
79
+ def self.contextual_predicates(text, tokens, terms)
80
+ surfaces, question = terms.fetch('contextual-phrase')
81
+ tokens.each_with_index.filter_map do |token, index|
82
+ next if token['unknown'] || token['pos'] != '動詞' || token['pos_detail'] != '自立'
83
+ pattern = CONTEXTUAL_PREDICATES.find do |prefix, lemma|
84
+ surfaces.include?(prefix + lemma) && token['lemma'] == lemma &&
85
+ token['offset'] >= prefix.length && text[token['offset'] - prefix.length...token['offset']] == prefix
86
+ end
87
+ next unless pattern
88
+ prefix, lemma = pattern
89
+ start = token['offset'] - prefix.length
90
+ prefix_index = tokens[0...index].rindex { |member| member['offset'] == start }
91
+ next unless prefix_index
92
+ members = tokens[prefix_index..predicate_end(tokens, index, text)]
93
+ next if members.any? { |member| member['unknown'] }
94
+ finish = members.last['end_offset']
95
+ metadata = { 'lemma' => prefix + lemma, 'part_of_speech' => '動詞',
96
+ 'negative' => negative?(members), 'detector' => 'mecab-ipadic' }
97
+ [start, finish, text[start...finish], 'contextual-phrase', question, metadata]
98
+ end
99
+ end
100
+
101
+ def self.negative?(members)
102
+ members.any? { |member| member['pos'] == '助動詞' && %w[ない ぬ ん].include?(member['lemma']) }
103
+ end
104
+
105
+ def self.scan_morphemes(findings, paragraph, text, tokens, settings, rules, terms, contextual = [])
72
106
  phrase_ranges = terms['contextual-phrase'][0].flat_map do |phrase|
73
107
  text.to_enum(:scan, Regexp.new(Regexp.escape(phrase))).map do
74
108
  match = Regexp.last_match
75
109
  match.begin(0)...match.end(0)
76
110
  end
77
111
  end
112
+ phrase_ranges.concat(contextual.map { |start, finish, *_| start...finish })
78
113
  tokens.each_with_index do |token, index|
79
114
  next if token['unknown'] || phrase_ranges.any? { |range| range.cover?(token['offset']) }
80
115
  lemma = token['lemma']
@@ -116,7 +151,7 @@ module AsciidocPubkit
116
151
  end
117
152
  next unless rule
118
153
  members = tokens[index..finish_index]
119
- negative = members.any? { |member| member['pos'] == '助動詞' && %w[ない ぬ ん].include?(member['lemma']) }
154
+ negative = negative?(members)
120
155
  next if rules.fetch('negative_only').include?(lemma) && !negative
121
156
  surface = text[token['offset']...tokens[finish_index]['end_offset']]
122
157
  allows = settings.fetch('allows')
@@ -143,22 +178,25 @@ module AsciidocPubkit
143
178
  finish
144
179
  end
145
180
 
146
- def self.scan_literal_terms(findings, paragraph, text, terms, settings, morphological:)
147
- candidates = []
181
+ def self.scan_literal_terms(findings, paragraph, text, terms, settings, morphological:, contextual: [])
182
+ candidates = contextual.dup
148
183
  terms.each do |rule, (surfaces, question)|
149
184
  next if morphological && !%w[generic-framing contextual-phrase].include?(rule)
150
185
  surfaces.each do |surface|
151
186
  text.to_enum(:scan, Regexp.new(Regexp.escape(surface))).each do
152
187
  match = Regexp.last_match
188
+ next if contextual.any? { |start, finish, _, candidate_rule, *_| start == match.begin(0) && finish == match.end(0) && candidate_rule == rule }
153
189
  candidates << [match.begin(0), match.end(0), surface, rule, question]
154
190
  end
155
191
  end
156
192
  end
157
193
  # Keep the most specific phrase, including when it is explicitly allowed.
158
- candidates.each do |start, finish, surface, rule, question|
194
+ candidates.each do |start, finish, surface, rule, question, metadata|
159
195
  next if candidates.any? { |left, right, *_| left <= start && right >= finish && right - left > finish - start }
160
- next if settings.fetch('allows').include?(surface)
196
+ forms = metadata ? [surface, surface + '。', metadata['lemma']] : [surface]
197
+ next if forms.any? { |form| settings.fetch('allows').include?(form) }
161
198
  add(findings, paragraph, text, start, surface, rule, 'hint', question)
199
+ findings.last.merge!(metadata) if metadata
162
200
  end
163
201
  end
164
202
 
@@ -9,7 +9,7 @@ require 'pathname'
9
9
  require 'optparse'
10
10
 
11
11
  module AsciidocPubkit
12
- VERSION = '0.6.1'
12
+ VERSION = '0.6.2'
13
13
  class Error < StandardError; end
14
14
 
15
15
  def self.hash_text(text)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoc-pubkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberGarage