asciidoc-pubkit 0.8.5 → 0.8.6

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: bcbb2a3c913ace085eb70388740dfc506fefd0ef064380c58e0ebbab63b422a8
4
- data.tar.gz: 7b643916c2b5c19f2264541f9e8660d212e7a351a1de10142087e4437b527f56
3
+ metadata.gz: e1fb57dff066c28a30d6205ad30ff8c5cd1a89fc27c35ec8379e9c80354843ae
4
+ data.tar.gz: 9431768603df5a8191ba93b0bc5773ad188050e06304e6f0f35272bbe132ef51
5
5
  SHA512:
6
- metadata.gz: 3cbda445b71d34817dfb51f64c49665106ae471ad98c0af3995697475f578fb658bdf2abc9e1894538a01d1e6e18b5d6fe850a9c818d868d8c2ad2663dd29598
7
- data.tar.gz: dd89673d6281bc418a127dcd14ab7273e4919d677a84ef38b3b9a431ade81aa3d7ff8615b3bcfeaeeebe4de9e4016e157cb7d43065dbb4d511ee19bdd3f48341
6
+ metadata.gz: 901984c71c8178ba62738e21ec67cb5585ebe72719ef06c03b38585c5ad7d0961a9fe4da78c47d1612e9c339d6170bee9a060ea4cf2cf68e9301ae4b80bb96ac
7
+ data.tar.gz: 70570e253745de217ba1d4eb2c5447a7a1dba2f72e3d1f5f1abb40787d54c996fa9dca3072bb190071a459346146200e5744e9eb5fa72f4410c9dd314af71333
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.6 — 2026-10-02
4
+
5
+ - Add separate `review scan --scope lists` sessions for source-mapped single-line
6
+ outline list text. Preserve markers, structure, inline tokens, and other scopes;
7
+ report unsupported or reused-source items as coverage notices.
8
+ - Add opt-in `--preserve-heading-ids` for heading scans. Save exact old-ID anchor
9
+ permissions and verify only those additions immediately before their selected
10
+ headings while retaining section IDs, structure, and protected body content.
11
+ - Advance sessions to schema 3; retain prior baselines and finish
12
+ ongoing reviews with their original tool and schema.
13
+
3
14
  ## 0.8.5 — 2026-10-02
4
15
 
5
16
  - Refine role-metaphor guidance for 入口: consider the actual creation, invocation,
data/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
 
8
8
  A toolkit for authoring and reviewing AsciiDoc books.
9
9
 
10
- Version 0.8.5 provides shared Japanese technical writing criteria, an authoring
11
- prompt, separate prose and heading review prompts, manuscript scoring, and
10
+ Version 0.8.6 provides shared Japanese technical writing criteria, an authoring
11
+ prompt, separate prose, heading, and list review prompts, manuscript scoring, and
12
12
  baseline verification for edited manuscripts, plus explicit prose-only mechanical
13
13
  replacements using a limited prh-format rule set.
14
14
  The CLI, diagnostics, documentation, and generated instructions are in English.
@@ -16,7 +16,7 @@ Japanese text is retained in manuscript excerpts, rule dictionaries, and fixture
16
16
 
17
17
  ## Available features
18
18
 
19
- The following commands are available in version 0.8.5.
19
+ The following commands are available in version 0.8.6.
20
20
 
21
21
  | Feature | Commands | Behavior |
22
22
  | --- | --- | --- |
@@ -24,6 +24,7 @@ The following commands are available in version 0.8.5.
24
24
  | [Replacement preview](#prose-replacements) | `replace check`, `replace diff` | Report mechanical replacement candidates or preview a diff for mapped running prose; preserve source files |
25
25
  | [Replacement application](#prose-replacements) | `replace apply` | Apply author-supplied rules after checking protected content and document structure; no MeCab or AI required |
26
26
  | [Prose review](#review-workflow) | `review scan` | Collect mapped Japanese running prose and contextual review candidates with MeCab/IPADIC by default |
27
+ | [List review](#list-review) | `review scan --scope lists` | Review mapped single-line list text in a separate session |
27
28
  | [Heading review](#heading-review) | `review scan --scope headings` | Collect editable section titles separately, retaining the outline and read-only body evidence |
28
29
  | [Review prompts](#prompt) | `review prompt` | Generate one Markdown prompt from a saved session for review and editing by an external reviewer |
29
30
  | [Baseline verification](#verify) | `review verify` | Check edited sources against a saved baseline for mechanical preservation; meaning is not verified |
@@ -107,7 +108,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
107
108
 
108
109
  ```ruby
109
110
  source 'https://rubygems.org'
110
- gem 'asciidoc-pubkit', '~> 0.8.5'
111
+ gem 'asciidoc-pubkit', '~> 0.8.6'
111
112
  ```
112
113
 
113
114
  Then install dependencies and run the CLI through Bundler:
@@ -129,7 +130,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
129
130
  cd asciidoc-pubkit
130
131
  bundle install
131
132
  gem build asciidoc-pubkit.gemspec
132
- gem install ./asciidoc-pubkit-0.8.5.gem
133
+ gem install ./asciidoc-pubkit-0.8.6.gem
133
134
  asciidoc-pubkit --version
134
135
  ```
135
136
 
@@ -153,7 +154,7 @@ asciidoc-pubkit writing prompt --lang ja --output writing-prompt.md
153
154
  ```
154
155
 
155
156
  For example, `--lang en` exits with an unsupported-language error. No English
156
- review or writing criteria are shipped in 0.8.5.
157
+ review or writing criteria are shipped in 0.8.6.
157
158
 
158
159
  For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
159
160
  paragraphs deliberately contain review candidates; its code block must remain
@@ -520,8 +521,9 @@ are frozen into the session; scan again after changing them.
520
521
 
521
522
  ### Heading review
522
523
 
523
- Review headings and prose in separate sessions. The default scan scope remains
524
- `prose`; `--scope headings` selects section titles. There is no combined scope.
524
+ Review headings, prose, and list text in separate sessions. The default scan scope
525
+ remains `prose`; `--scope headings` selects section titles and `--scope lists` selects
526
+ supported list text. There is no combined scope.
525
527
  Default output directories are `.pubkit/review` for prose and `.pubkit/headings`
526
528
  for headings, so separate scans do not replace each other by default.
527
529
  The scope is saved at scan time; `prompt` and `verify` use that saved scope.
@@ -564,12 +566,25 @@ and converted inline titles and physical title lines reused by multiple includes
564
566
  Unresolved section titles receive coverage notices. Verification permits only selected title text changes while
565
567
  protecting the body, title markers, hierarchy, order, section IDs, references,
566
568
  attributes, includes and other protected content. A title-derived section ID
567
- change fails verification. Establish stable explicit IDs before scanning when
568
- needed; this tool does not add IDs or migrate references. Numeric title changes
569
+ change fails verification. To permit limited ID preservation, scan with
570
+ `--scope headings --preserve-heading-ids`. Eligible generated headings then save
571
+ an exact `permitted_id_anchor` in their metadata. A reviewer may insert only that
572
+ anchor immediately before its selected heading, for example:
573
+
574
+ ```adoc
575
+ [#_手動圧縮の入口]
576
+ == 手動圧縮と拡張からの起動
577
+ ```
578
+
579
+ Verification allows this exact addition while requiring the same section ID,
580
+ hierarchy, order, and protected body. Existing explicit anchors remain protected;
581
+ other anchors and reference migrations are outside this permission. Without the
582
+ flag, only title text is editable. Scan and prompt never insert anchors or rewrite
583
+ manuscripts. Numeric title changes
569
584
  are manual-review notices, and remaining candidates do not fail verification.
570
585
  `meaning_verified` remains `false`.
571
586
 
572
- The session schema is now 2. Earlier sessions must be recreated; preserve or
587
+ Version 0.8.6 uses session schema 3. Earlier sessions must be recreated; preserve or
573
588
  finish an ongoing review with its original tool before establishing a new baseline.
574
589
  `review score` continues to support prose only and rejects `--scope`.
575
590
 
@@ -1124,12 +1139,13 @@ reports analyzer changes instead of treating results from different dictionaries
1124
1139
  as directly comparable. Prompt generation uses saved evidence and does not need
1125
1140
  MeCab. Changed rules or dictionary settings require a new scan.
1126
1141
 
1127
- Sessions from earlier tool versions are not compatible with 0.8.5 (session schema 2). Keep the original baseline for
1142
+ Version 0.8.6 uses session schema 3; version 0.8.5 uses schema 2.
1143
+ Sessions with a different schema or tool version cannot be loaded. Keep the original baseline for
1128
1144
  an ongoing review and finish it with the original version, or start a new review
1129
1145
  pass in a different directory:
1130
1146
 
1131
1147
  ```sh
1132
- asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.5
1148
+ asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.6
1133
1149
  ```
1134
1150
 
1135
1151
  Severity describes review priority, not proof of an error. There is no AI-authorship
@@ -1147,6 +1163,33 @@ unambiguously, the paragraph is skipped with a coverage notice. Columns are
1147
1163
  one-based Unicode character positions, not byte offsets or display widths.
1148
1164
  An empty findings array does not establish full coverage or good prose.
1149
1165
 
1166
+ ## List review
1167
+
1168
+ Use a separate session to review list descriptions, including reference-list text:
1169
+
1170
+ ```sh
1171
+ asciidoc-pubkit review scan book.adoc --scope lists --output .pubkit/lists
1172
+ asciidoc-pubkit review prompt .pubkit/lists --output lists-review.md
1173
+ asciidoc-pubkit review verify .pubkit/lists
1174
+ ```
1175
+
1176
+ The default output directory is `.pubkit/lists`. This scope reuses prose rules,
1177
+ glossary, style, analyzer, and shared writing criteria. It does not include running
1178
+ prose or edit headings. Each selected item appears once with heading context;
1179
+ read additional evidence separately when its technical role cannot be established.
1180
+
1181
+ Only exact source-mapped, single-line simple unordered or ordered item text is
1182
+ editable, including supported nested outline lists. Multiline, description,
1183
+ checklist, compound, ambiguous, and reused-source items remain protected with
1184
+ coverage notices. List markers, nesting, structure, links, and other inline tokens
1185
+ remain protected. Candidate columns include the source marker and indentation.
1186
+ Session JSON retains the `paragraphs` and `paragraph_id` keys, with
1187
+ `kind: list-item` and source `column`, `prefix`, and `suffix` metadata.
1188
+
1189
+ Default prose scanning, scoring, and mechanical replacement keep their existing
1190
+ prose scope. Heading body evidence continues to omit lists. A successful verify
1191
+ checks preservation and does not establish editorial completion.
1192
+
1150
1193
  ## Development
1151
1194
 
1152
1195
  ```sh
@@ -21,8 +21,9 @@ For a title such as `手動圧縮の入口` or `画像・分類モデルへの
21
21
  shared role-metaphor criteria to the section evidence. Consider `起動方法` or
22
22
  `呼び出しAPI` only when that is what the body explains. Naming a subject does
23
23
  not by itself explain the operation. If a better title would change a generated
24
- section ID, record the proposed title and ID concern for a separate authorized
25
- migration rather than changing anchors or keeping it for stylistic reasons.
24
+ section ID, use the exact permitted_id_anchor only when the saved session explicitly
25
+ authorizes it. Otherwise record the proposed title and ID concern for separate
26
+ authorized preservation. Protection does not justify an editorial keep decision.
26
27
 
27
28
  ## Compare headings with the same role
28
29
 
@@ -8,7 +8,7 @@ module AsciidocPubkit
8
8
  Commands:
9
9
  writing criteria Print the shared Japanese prose criteria
10
10
  writing prompt Generate a Japanese technical writing prompt
11
- review scan FILE Collect Japanese prose or headings and review candidates
11
+ review scan FILE Collect Japanese prose, headings, or list text and review candidates
12
12
  review score FILE Score Japanese prose; optionally use a local AI CLI
13
13
  review prompt SESSION Generate an English review prompt with Japanese source excerpts
14
14
  review verify SESSION Compare edited sources with the saved baseline
@@ -45,7 +45,7 @@ module AsciidocPubkit
45
45
  parser = OptionParser.new do |opts|
46
46
  input_name = group == 'writing' ? '' : ((group == 'replace' || %w[scan score].include?(command)) ? ' FILE' : ' SESSION')
47
47
  opts.banner = "Usage: asciidoc-pubkit #{group} #{command} [options]#{input_name}"
48
- opts.on('-o', '--output PATH', group == 'review' && command == 'scan' ? 'New session directory (default: .pubkit/review for prose, .pubkit/headings for headings)' : 'New output file (default: standard output)') { |v| options[:output] = v }
48
+ opts.on('-o', '--output PATH', group == 'review' && command == 'scan' ? 'New session directory (default: .pubkit/review, .pubkit/headings, or .pubkit/lists)' : 'New output file (default: standard output)') { |v| options[:output] = v }
49
49
  opts.on('--lang LANG', 'Language (ja only in this release)') { |v| options[:language] = v }
50
50
  if group == 'replace'
51
51
  opts.on('--rules FILE', 'Required prh-format replacement rules (limited compatibility)') { |v| options[:rules] = v }
@@ -54,7 +54,8 @@ module AsciidocPubkit
54
54
  opts.on('--only FILE', 'Select one included file') { |v| options[:only] = v }
55
55
  elsif group == 'review' && %w[scan score].include?(command)
56
56
  if command == 'scan'
57
- opts.on('--scope SCOPE', 'prose (default) or headings; separate review sessions') { |v| options[:scope] = v }
57
+ opts.on('--scope SCOPE', 'prose (default), headings, or lists; separate review sessions') { |v| options[:scope] = v }
58
+ opts.on('--preserve-heading-ids', 'Allow exact old-ID anchors before eligible headings') { options[:preserve_heading_ids] = true }
58
59
  opts.on('--heading-rules FILE', 'Replace default heading rules with a YAML rule set') { |v| options[:heading_rules] = v }
59
60
  opts.on('-y', '--yes', 'Answer yes to replacement confirmation') { options[:yes] = true }
60
61
  opts.on('--no-input', 'Never prompt; fail on existing output unless --yes') { options[:no_input] = true }
@@ -128,6 +129,8 @@ module AsciidocPubkit
128
129
  end
129
130
  if result['scope'] == 'headings'
130
131
  out.puts "Scanned #{result['headings']} headings; found #{result['findings']} review candidates."
132
+ elsif result['scope'] == 'lists'
133
+ out.puts "Scanned #{result['paragraphs']} list items; found #{result['findings']} review candidates."
131
134
  else
132
135
  out.puts "Scanned #{result['paragraphs']} paragraphs; found #{result['findings']} review candidates."
133
136
  end
@@ -64,6 +64,15 @@ module AsciidocPubkit
64
64
  true
65
65
  end
66
66
  nodes.each { |node| collect(node) }
67
+ if scope == 'lists'
68
+ reused = @paragraphs.group_by { |p| [p['file'], p['line']] }.select { |_, group| group.length > 1 }
69
+ @paragraphs.reject! do |paragraph|
70
+ next false unless reused.key?([paragraph['file'], paragraph['line']])
71
+ @coverage << { 'context' => 'list_item', 'file' => paragraph['file'], 'line' => paragraph['line'],
72
+ 'reason' => 'The source list item is reused and cannot be edited independently.' }
73
+ true
74
+ end
75
+ end
67
76
  ensure
68
77
  Asciidoctor::LoggerManager.logger = previous_logger
69
78
  end
@@ -110,14 +119,80 @@ module AsciidocPubkit
110
119
  @coverage << { 'context' => 'heading', 'file' => path, 'line' => line, 'reason' => 'Excluded by configuration.' }
111
120
  return
112
121
  end
122
+ section_id = node.id
123
+ generated = section_id && !node.attributes.key?('id')
124
+ anchor = if @settings['preserve_heading_ids'] && generated && section_id.match?(/\A[\p{L}\p{N}_.:-]+\z/)
125
+ "[##{section_id}]"
126
+ end
113
127
  @headings << { 'id' => AsciidocPubkit.hash_text([path, line, match[2]].join("\0"))[0, 16],
114
128
  'index' => index, 'parent_index' => parent_index,
115
129
  'file' => path, 'line' => line, 'column' => match[1].length + 1,
116
130
  'prefix' => match[1], 'suffix' => match[3], 'level' => node.level,
117
- 'section_id' => node.id, 'text' => match[2] }
131
+ 'section_id' => section_id, 'id_generated' => !!generated, 'permitted_id_anchor' => anchor, 'text' => match[2] }
132
+ end
133
+
134
+ def collect_list_item(node)
135
+ cursor = node.source_location
136
+ path, line = cursor && [cursor.file, cursor.lineno]
137
+ parent = node.parent
138
+ supported = %i[ulist olist].include?(parent.context)
139
+ while parent
140
+ supported = false if %i[quote verse table listing literal pass dlist colist].include?(parent.context)
141
+ parent = parent.parent
142
+ end
143
+ # ListItem#text applies substitutions; read through the public subs API.
144
+ original_subs = node.subs.dup
145
+ begin
146
+ node.subs.clear
147
+ text = node.text
148
+ ensure
149
+ node.subs.replace(original_subs)
150
+ end
151
+ raw = path && @source_lines[path] && @source_lines[path][line - 1]
152
+ pattern = node.marker && /\A([ \t]*#{Regexp.escape(node.marker)}[ \t]+)(.*?)([ \t]*)\z/
153
+ match = raw && pattern && raw.match(pattern)
154
+ # Include boundaries can leave the cursor on the include directive.
155
+ # Recover a unique physical span; repeated use is rejected after collection.
156
+ if supported && node.simple? && text && !text.include?("\n") && pattern && (!match || match[2] != text)
157
+ candidates = @source_lines.flat_map do |source_path, lines|
158
+ lines.each_with_index.filter_map do |source_line, offset|
159
+ candidate = source_line.match(pattern)
160
+ [source_path, offset + 1, candidate] if candidate && candidate[2] == text
161
+ end
162
+ end
163
+ path, line, match = candidates.first if candidates.length == 1
164
+ end
165
+ unless supported && node.simple? && text && !text.include?("\n") && match && match[2] == text && !text.empty?
166
+ @coverage << { 'context' => 'list_item', 'file' => path, 'line' => line,
167
+ 'reason' => 'Only unambiguously mapped, single-line outline list text with simple content is editable.' }
168
+ return
169
+ end
170
+ return if @only && @only != path
171
+ relative = Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
172
+ return if @settings.fetch('exclude').any? { |glob| File.fnmatch?(glob, relative, File::FNM_PATHNAME | File::FNM_DOTMATCH) }
173
+ headings = []
174
+ section_index = nil
175
+ parent = node.parent
176
+ while parent
177
+ if parent.context == :section && parent.title
178
+ headings.unshift(parent.title)
179
+ section_index ||= @section_indexes[parent]
180
+ end
181
+ parent = parent.parent
182
+ end
183
+ @paragraphs << {
184
+ 'id' => AsciidocPubkit.hash_text([path, line, text].join("\0"))[0, 16],
185
+ 'file' => path, 'line' => line, 'end_line' => line, 'column' => match[1].length + 1,
186
+ 'prefix' => match[1], 'suffix' => match[3], 'kind' => 'list-item',
187
+ 'section_index' => section_index, 'headings' => headings, 'text' => text
188
+ }
118
189
  end
119
190
 
120
191
  def collect(node)
192
+ if scope == 'lists'
193
+ collect_list_item(node) if node.context == :list_item
194
+ return
195
+ end
121
196
  if %i[list_item table quote verse listing literal pass].include?(node.context)
122
197
  @coverage << { 'context' => node.context.to_s, 'reason' => 'Not reviewed as running prose.' }
123
198
  end
@@ -213,7 +213,7 @@ module AsciidocPubkit
213
213
  'id' => AsciidocPubkit.hash_text([paragraph['id'], rule, offset, match].join(':'))[0, 16],
214
214
  'paragraph_id' => paragraph['id'], 'rule' => rule, 'severity' => severity,
215
215
  'file' => paragraph['file'], 'line' => paragraph['line'] + prefix.count("\n"),
216
- 'column' => (prefix.rindex("\n") ? prefix.length - prefix.rindex("\n") : prefix.length + 1),
216
+ 'column' => (prefix.rindex("\n") ? prefix.length - prefix.rindex("\n") : prefix.length + paragraph.fetch('column', 1)),
217
217
  'match' => match, 'question' => question
218
218
  }
219
219
  end
@@ -4,7 +4,7 @@ require 'tmpdir'
4
4
 
5
5
  module AsciidocPubkit
6
6
  class Session
7
- SCHEMA = 2
7
+ SCHEMA = 3
8
8
 
9
9
  def self.scan(entry, options)
10
10
  settings = Settings.new(entry, options)
@@ -12,7 +12,7 @@ module AsciidocPubkit
12
12
  unless document.diagnostics.empty?
13
13
  raise Error, "Document diagnostics must be resolved before scanning:\n" + document.diagnostics.map { |d| "#{d['severity']}: #{d['message']}" }.join("\n")
14
14
  end
15
- default_output = settings.data['scope'] == 'headings' ? '.pubkit/headings' : '.pubkit/review'
15
+ default_output = { 'headings' => '.pubkit/headings', 'lists' => '.pubkit/lists' }.fetch(settings.data['scope'], '.pubkit/review')
16
16
  destination = File.expand_path(options.fetch(:output, default_output))
17
17
  replacing = File.exist?(destination) || File.symlink?(destination)
18
18
  if replacing
@@ -95,13 +95,18 @@ module AsciidocPubkit
95
95
  File.write(path, JSON.pretty_generate(value) + "\n")
96
96
  end
97
97
 
98
- def self.protected_content(document)
98
+ def self.protected_content(document, allowed_anchor_lines: {})
99
99
  document.sources.to_h do |path, raw|
100
100
  paragraphs = document.paragraphs.select { |p| p['file'] == path }
101
101
  heading_scope = document.scope == 'headings'
102
102
  editable_headings = document.headings.select { |h| h['file'] == path }.to_h { |h| [h['line'], h] }
103
+ editable_items = document.scope == 'lists' ? paragraphs.to_h { |p| [p['line'], p] } : {}
103
104
  editable_lines = heading_scope ? [] : paragraphs.flat_map { |p| (p['line']..p['end_line']).to_a }
104
105
  outside = raw.lines.each_with_index.filter_map do |line, index|
106
+ next if allowed_anchor_lines.fetch(path, []).include?(index + 1)
107
+ if (item = editable_items[index + 1])
108
+ next [item['prefix'], '<editable-list-text>', item['suffix']]
109
+ end
105
110
  next if editable_lines.include?(index + 1)
106
111
  if heading_scope && (heading = editable_headings[index + 1])
107
112
  # Keep a positional placeholder: removing entire lines would hide reordering.
@@ -160,6 +165,7 @@ module AsciidocPubkit
160
165
  raise Error, 'Sources have changed since scanning. Create a new scan before generating a prompt.' unless stale.empty?
161
166
  language = Language.validate!(@manifest.fetch('settings').fetch('language'), operation: 'review')
162
167
  return heading_prompt(mode, language) if @manifest.fetch('settings').fetch('scope', 'prose') == 'headings'
168
+ list_scope = @manifest['settings']['scope'] == 'lists'
163
169
  criteria = @manifest.fetch('writing_criteria')
164
170
  instructions = <<~TEXT
165
171
  # Japanese manuscript review
@@ -184,7 +190,7 @@ module AsciidocPubkit
184
190
  After editing, reread each paragraph in context. Report unresolved issues and do not claim publication readiness.
185
191
  Mechanical verification does not establish semantic correctness.
186
192
 
187
- #{review_process('paragraph', mode)}
193
+ #{review_process(list_scope ? 'list item' : 'paragraph', mode)}
188
194
 
189
195
  ## Shared prose criteria
190
196
 
@@ -213,6 +219,18 @@ module AsciidocPubkit
213
219
  Coverage notices: #{@document['coverage'].length}
214
220
 
215
221
  TEXT
222
+ if list_scope
223
+ instructions.sub!('running-prose paragraphs', 'single-line outline list item text')
224
+ instructions.sub!('proposed paragraph revisions', 'proposed list item revisions')
225
+ instructions.sub!('Do not change excluded content, lists, tables, quotations, or other files.',
226
+ 'Preserve list markers, nesting, order, continuation syntax, and inline tokens. Do not edit running prose, excluded list items, tables, quotations, or other files.')
227
+ instructions.sub!('running-prose edit scope', 'selected list-text edit scope')
228
+ instructions.sub!('Only source-mapped running-prose paragraphs are reviewed.', 'Only unambiguously source-mapped, single-line simple unordered or ordered list text is reviewed.')
229
+ instructions.sub!('Headings, lists, tables, quotations, code, and passthrough blocks are not prose-reviewed in this release.',
230
+ 'Multiline, description, checklist, compound, and reused-source list items remain protected. Running prose, headings, tables, quotations, code, and passthrough blocks are not reviewed in this session.')
231
+ instructions.sub!('the supplied prose/body context omits lists and other excluded blocks',
232
+ 'this session supplies only selected list text and heading labels; running prose and other excluded blocks are omitted')
233
+ end
216
234
  instructions << <<~TEXT
217
235
  ## Reading order
218
236
 
@@ -232,7 +250,7 @@ module AsciidocPubkit
232
250
  instructions << "## Context\n\n#{prompt_data(JSON.generate(context), 'json')}\n"
233
251
  previous_context = context
234
252
  end
235
- instructions << "### Paragraph #{paragraph['id']} — lines #{paragraph['line']}–#{paragraph['end_line']}\n\n"
253
+ instructions << "### #{list_scope ? 'List item' : 'Paragraph'} #{paragraph['id']} — lines #{paragraph['line']}–#{paragraph['end_line']}\n\n"
236
254
  instructions << prompt_data(paragraph['text'], 'text') << "\n"
237
255
  related = findings_by_paragraph.fetch(paragraph['id'], [])
238
256
  if related.empty?
@@ -252,7 +270,7 @@ module AsciidocPubkit
252
270
  document.diagnostics.each { |d| issues << { 'kind' => 'parse-diagnostic', 'message' => d['message'] } }
253
271
  old_paths = @manifest['sources'].map { |source| source['path'] }.sort
254
272
  issues << { 'kind' => 'source-set-changed', 'message' => 'The included source file set changed.' } if old_paths != document.sources.keys.sort
255
- current = self.class.protected_content(document)
273
+ current = self.class.protected_content(document, allowed_anchor_lines: permitted_anchor_lines(document))
256
274
  @manifest['protected'].each do |path, saved|
257
275
  next if current[path] == saved
258
276
  issues << { 'kind' => 'protected-content-changed', 'file' => path,
@@ -288,6 +306,21 @@ module AsciidocPubkit
288
306
 
289
307
  private
290
308
 
309
+ def permitted_anchor_lines(document)
310
+ return {} unless @manifest['settings']['scope'] == 'headings' && @manifest['settings']['preserve_heading_ids']
311
+ allowed = Hash.new { |hash, path| hash[path] = [] }
312
+ @document.fetch('headings', []).each do |saved|
313
+ anchor = saved['permitted_id_anchor']
314
+ next unless anchor
315
+ current = document.headings.find { |heading| heading['index'] == saved['index'] && heading['file'] == saved['file'] }
316
+ next unless current && current['section_id'] == saved['section_id']
317
+ line = current['line'] - 1
318
+ next unless line.positive? && document.sources.fetch(current['file']).lines[line - 1].chomp == anchor
319
+ allowed[current['file']] << line
320
+ end
321
+ allowed
322
+ end
323
+
291
324
  def comparison_structure(structure, headings, heading_scope)
292
325
  editable = headings.map { |h| h['index'] }
293
326
  structure.each_with_index.filter_map do |node, index|
@@ -311,7 +344,11 @@ module AsciidocPubkit
311
344
  Preserve section count, order, hierarchy, IDs, references, attributes, include directives,
312
345
  inline tokens, title markers, and all content outside the selected title spans.
313
346
  Title-derived section IDs can change after a title edit. Keep IDs unchanged;
314
- otherwise report the concern and leave the title for a separate ID migration.
347
+ when permitted_id_anchor is present in the saved heading metadata, you may insert
348
+ that exact anchor on the line immediately before its heading to preserve the old ID.
349
+ This is the only authorized addition outside title spans. Do not edit existing anchors,
350
+ invent IDs, or add anchors for other headings. Without that permission, report the
351
+ concern and leave the title for a separately authorized ID preservation task.
315
352
  Preserve the configured fixed section names; do not rename them merely for variety.
316
353
  Automated findings are review candidates, not proven defects.
317
354
  Do not invent facts, guarantees, implementation mechanisms or benefits.
@@ -343,7 +380,8 @@ module AsciidocPubkit
343
380
 
344
381
  ## Coverage and reading order
345
382
 
346
- Only selected, unambiguously source-mapped plain ATX section titles are editable.
383
+ Only selected, unambiguously source-mapped plain ATX section titles and explicitly
384
+ permitted old-ID anchor additions are editable.
347
385
  Document titles, old-style headings, converted inline titles and attribute-expanded
348
386
  titles remain protected. Body coverage excludes lists, tables, quotations and code;
349
387
  use needs-evidence when the supplied body cannot establish a title's scope.
@@ -31,20 +31,24 @@ module AsciidocPubkit
31
31
  base = @path ? File.dirname(@path) : File.dirname(File.expand_path(entry))
32
32
  language = Language.validate!(options[:language] || review.fetch('language', Language::DEFAULT), operation: 'review')
33
33
  scope = options.fetch(:scope, 'prose')
34
- raise Error, 'scope must be prose or headings.' unless %w[prose headings].include?(scope)
34
+ raise Error, 'scope must be prose, headings, or lists.' unless %w[prose headings lists].include?(scope)
35
35
  if scope == 'headings' && (options[:rules] || options[:style])
36
36
  raise Error, 'Heading scans use --heading-rules, not prose --rules or --style.'
37
37
  end
38
- if scope == 'prose' && options[:heading_rules]
38
+ if scope != 'headings' && options[:preserve_heading_ids]
39
+ raise Error, '--preserve-heading-ids requires --scope headings.'
40
+ end
41
+ if scope != 'headings' && options[:heading_rules]
39
42
  raise Error, '--heading-rules requires --scope headings.'
40
43
  end
41
44
  heading_rules_path = options[:heading_rules] ? File.expand_path(options[:heading_rules]) : (review['heading_rules'] ? File.expand_path(review['heading_rules'], base) : HeadingRuleSet.default_path(language))
42
45
  rules_path = options[:replacement] ? RuleSet.default_path(language) : options[:rules] ? File.expand_path(options[:rules]) : (review['rules'] ? File.expand_path(review['rules'], base) : RuleSet.default_path(language))
43
46
  @data = {
44
47
  'scope' => scope,
48
+ 'preserve_heading_ids' => !!options[:preserve_heading_ids],
45
49
  'heading_rules' => scope == 'headings' ? HeadingRuleSet.load(heading_rules_path) : nil,
46
50
  'heading_rules_path' => heading_rules_path,
47
- 'rules' => scope == 'prose' ? RuleSet.load(rules_path) : nil,
51
+ 'rules' => scope != 'headings' ? RuleSet.load(rules_path) : nil,
48
52
  'rules_path' => rules_path,
49
53
  'language' => language,
50
54
  'style' => options[:style] || review.fetch('style', 'preserve'),
@@ -9,7 +9,7 @@ require 'pathname'
9
9
  require 'optparse'
10
10
 
11
11
  module AsciidocPubkit
12
- VERSION = '0.8.5'
12
+ VERSION = '0.8.6'
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.8.5
4
+ version: 0.8.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberGarage
@@ -37,8 +37,8 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.0'
40
- description: Generate Japanese technical writing prompts, review AsciiDoc prose and
41
- section headings separately, verify protected manuscript content, and explicitly
40
+ description: Generate Japanese technical writing prompts, review AsciiDoc prose, section
41
+ headings, and list text separately, verify protected manuscript content, and explicitly
42
42
  apply prose-only mechanical replacements.
43
43
  executables:
44
44
  - asciidoc-pubkit