Skip to content

Commit 0aebd94

Browse files
Phase 10: deviation reconciliation — the repairs, three gates and the as-built ledger (#102)
* feat: repair the core defects phase 9 and the reviews found Status maps every three-place code with #standard? for the protocol range (HTTP-10); Protocol admits HTTP/1.0 (HTTP-33); URL.parse! wraps every URI error and refuses a host-less http URL (HTTP-47); Model.own drops a default proc; the HeaderSyntax predicates are total; a closed stream is refused at Body.stream; the NaN and Complex duration guards; the configuration chain no longer nests per configure (CFG-13); the async logging step stops counting the caller's continuation; the proxy warning's credential belt is unanchored (OBS-11, CFG-22). Every shipped signature gains the SPDX header (NFR-13). The runtime manifest gains Protocol::HTTP_1_0 and Status#standard?; the pins the repairs invalidated move with them. * feat: map an HTTP/1.0 or vendor-status head in both transports Both ResponseMappers now hand HTTP/1.0 and any three-place status to the model (TRANSPORT-24); their YARD says so, and net_http's stray @decode_content tag is backticked. The adapter pins that used HTTP/1.0 as the unmappable head now use HTTP/1.2. Every shipped signature in the four adapter gems gains the SPDX header (NFR-13). * feat: fold TransportSuite onto Runner and fix the gem-name rule TransportSuite.run delegates to Runner.run, tearing its fresh cases down through the around wrapper so Runner is not widened. The default gem-name rule in PackagingCase reaches all six namespaces: -core is the root and http/json are acronyms (NFR-15). The NFR-13 pin over this repository's own signatures now expects :passed. * feat: add gates:ledger_audit, gates:spdx_rbs and gates:sole_parse Twenty-four blocking gates, the three new ones in DEFAULT_GATES and in CI's gates job, each with a fixture workspace that turns it red. The require allowlist refuses a dexpace/ path with a dot segment, and the rubocop gate runs with --ignore-parent-exclusion. * feat: add the probe's ninth check, chapters A requirement ID attributed to a product-spec chapter that does not carry it, read clause by clause, with ranges expanded and negations skipped. * feat: flip docs/deviations.md to as-built verdicts All nineteen rows carry a verdict citing gems/ paths and defined constants, and the amendment set is C1-C18. It rides here because gates:ledger_audit reads it at run time. * fix: close the proxy prefix leak, total Status, forward-bind chapters Review round 0 of phase 10 found four code-layer defects. R0-4: the proxy warning belt stopped at the first '/', '?' or '#', so a password holding one leaked its prefix into Kernel#warn and the config sink. ProxyResolution now scrubs the raw value from the authority's start through the LAST '@' before the redactor runs. R0-5: Status.of still threw outside 0..999, narrower than HTTP-10's "any code MUST return a Status". Construction is now total over Integer; #standard? stays the protocol range. R0-6: docs/deviations.md row 10 cited send(:new) as the bypass, but it runs the validating #initialize. The row is now "confirmed, narrower" (allocate and duck typing remain) with amendment C19. R0-8: the chapters check listed "appears in" as a negation, blinding it to the usual positive attribution. An ID run followed by that phrase now binds forward to the chapter it introduces. * fix: give ledger_audit's disable a reason, negate "does not appear" The one rubocop:disable phase 10 added in tools/ledger_audit.rb carried no reason; it now says why the row check threads the root and the resolver (review round 1, R1-3). The chapters check's forward "appears in" binding turned a negative sentence ("SEAM-13 does not appear in <chapter>") into a positive attribution. NEGATION gains "not appear in", "does not appear" and "do not appear" (R1-4). * fix: read never-appear and n't-appear as chapter negations Review round 2 (R2-2) found two more negated spellings of 'appears in' that bound forward and would fire as a wrong attribution: 'never appears in' and 'doesn't appear in'. Add 'never appear' and "n't appear" to the chapters check's NEGATION union; 'appears nowhere' was already there.
1 parent b242de6 commit 0aebd94

378 files changed

Lines changed: 1856 additions & 165 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# frozen_string_literal: true
2+
3+
# SPDX-License-Identifier: MIT
4+
# .claude/skills/housekeeping/chapters.rb
5+
#
6+
# The probe's ninth check, `chapters` (phase 10's plan, Task 7; design R10 and addendum A10).
7+
#
8+
# A document asserting that a requirement ID is stated in a named `docs/product-spec/` chapter,
9+
# where that chapter does not carry the ID. The unit is a CLAUSE, not a line: "A.md for X, Y;
10+
# B.md for Z" pairs every ID with every chapter under a same-line rule, which is why a naive rule
11+
# fires 23 times for 4 real defects (phase 10's Fact 6). So a line is split at `;`, each ID is
12+
# associated with the NEAREST PRECEDING chapter reference in its clause -- or, for an ID run
13+
# followed by "appears in", with the chapter that phrase introduces -- a range is expanded
14+
# whether or not its endpoints are backticked, and a line whose two-line window carries a negation
15+
# -- prose whose own subject is that the ID is NOT in that chapter -- is skipped. Appendix C
16+
# carries every ID and can never be wrong about one, so it is exempt as a target.
17+
#
18+
# Two stated blind spots, printed in the check's own `GAPS` so a reader is never told the check
19+
# saw something it did not: `continued_clause` (a chapter reference on the PRECEDING line is
20+
# invisible to a line-oriented scanner -- 8c's design :70, `SEAM-15`, was caught by hand), and
21+
# `dynamic_chapter_path` (a chapter named by an ellipsis such as `13-…md`, or by a variable,
22+
# resolves to no file and is skipped).
23+
24+
module Housekeeping
25+
module Checks
26+
# 9. A requirement ID attributed to a spec chapter that does not carry it.
27+
class Chapters < Check
28+
NAME = 'chapters'
29+
PREFIXES = %w[SEAM HTTP IO BODY CTX PIPE RECOV RETRY REDIR AUTH PAGE SSE SERDE OBS CFG
30+
TRANSPORT ASYNC XCUT NFR].freeze
31+
ID = /\b(#{PREFIXES.join('|')})-(\d+)\b/
32+
# `SEAM-11`–`SEAM-15`, SEAM-11–SEAM-15, SEAM-11–15: backticks tolerated on either end,
33+
# which is what hid 8a's `SEAM-13` from a range pattern written for bare text.
34+
RANGE = /`?\b(#{PREFIXES.join('|')})-(\d+)`?\s*[–—]\s*`?(?:\1-)?(\d+)\b`?/
35+
CHAPTER = %r{docs/product-spec/(\d\d-[a-z0-9-]+\.md)}
36+
BOUNDARY = ';'
37+
# Prose whose subject is that the ID is NOT in the named chapter, over the line and the
38+
# next one, because such a sentence routinely crosses a line break.
39+
# A union of plain phrases, never an /x pattern: under /x the spaces inside a phrase are
40+
# stripped and "appears nowhere" silently becomes "appearsnowhere" (found writing this).
41+
NEGATION = Regexp.union(
42+
'appear nowhere', 'appears nowhere', 'harvested nowhere', 'appear in no', 'appears in no',
43+
'appendix C is their only', 'appendix C is its only', 'appendix C alone',
44+
'does not carry', 'do not carry', 'carries neither', 'carries none', 'carries no ',
45+
'carry none', 'no prose chapter', 'appendix-C row', 'unfollowable', 'from appendix C',
46+
'read out of appendix C', 'read out of **appendix C', 'neither ID appears', 'is not in',
47+
'are not in', 'not stated in', 'only normative statement', 'only prose home',
48+
'not appear in', 'does not appear', 'do not appear', 'never appear', "n't appear"
49+
).then { |union| Regexp.new(union.source, Regexp::IGNORECASE) }
50+
# "X appears in <chapter>": the one verb that puts the chapter AFTER the IDs it is about.
51+
# An ID run followed by it binds FORWARD, to the chapter the verb introduces, instead of to
52+
# the nearest preceding one. Phase 10's review round 0 (R0-8) found the phrase listed as a
53+
# negation, which silenced this -- the usual positive attribution -- to hide one line of the
54+
# phase-5 segmentation design (:538-539) whose second run bound backward to the first
55+
# run's chapter; binding forward is what that line needed.
56+
FORWARD = /\bappears?\s+in\b/i
57+
EXEMPT_TARGET = 'appendix-c-consolidated-normative-requirement-index.md'
58+
# Documents that quote the pre-correction attributions ON PURPOSE, as the record of the
59+
# defect this check exists for: phase 10's own design, plan and checklist.
60+
EXEMPT_DOCUMENTS = %r{\Adocs/work/mvp/phase10/}
61+
SCANNED = ['docs'].freeze
62+
SKIPPED = %r{\Adocs/(?:product-spec|knowledge/harvested)/}
63+
GAPS = %i[continued_clause dynamic_chapter_path].freeze
64+
65+
def run(repo)
66+
carried = chapter_ids(repo)
67+
documents(repo).flat_map { |path| scan(path, repo.read(path).lines, carried) }
68+
end
69+
70+
# The pairs a single line asserts, [chapter, id], in clause order. Public so a test can
71+
# read the clause scoping directly.
72+
def self.pairs(line)
73+
line.split(BOUNDARY).flat_map { |clause| clause_pairs(clause) }
74+
end
75+
76+
# A run of consecutive IDs binds to the nearest preceding chapter, unless the run is
77+
# followed by FORWARD, when it binds to the chapter immediately after the verb (or to
78+
# nothing, when the chapter is on the next line: the continued-clause gap).
79+
def self.clause_pairs(clause)
80+
list = tokens(clause)
81+
chapter = nil
82+
pairs = []
83+
index = 0
84+
while index < list.length
85+
kind, value = list[index]
86+
if kind == :chapter
87+
chapter = value
88+
index += 1
89+
elsif kind == :forward
90+
index += 1
91+
else
92+
run_end = index
93+
run_end += 1 while run_end < list.length && list[run_end][0] == :id
94+
target = run_target(list, run_end, chapter)
95+
list[index...run_end].each { |_kind, id| pairs << [target, id] } if target
96+
index = run_end
97+
end
98+
end
99+
pairs
100+
end
101+
102+
def self.run_target(list, after, chapter)
103+
return chapter unless list.dig(after, 0) == :forward
104+
105+
list.dig(after + 1, 0) == :chapter ? list.dig(after + 1, 1) : nil
106+
end
107+
108+
# Chapters and IDs in textual order, every range expanded in place.
109+
def self.tokens(clause)
110+
found = []
111+
clause.scan(CHAPTER) { found << [Regexp.last_match.begin(0), :chapter, Regexp.last_match[1]] }
112+
clause.scan(FORWARD) { found << [Regexp.last_match.begin(0), :forward, nil] }
113+
covered = []
114+
clause.scan(RANGE) do
115+
match = Regexp.last_match
116+
covered << (match.begin(0)...match.end(0))
117+
(match[2].to_i..match[3].to_i).each { |n| found << [match.begin(0), :id, "#{match[1]}-#{n}"] }
118+
end
119+
clause.scan(ID) do
120+
match = Regexp.last_match
121+
next if covered.any? { |span| span.cover?(match.begin(0)) }
122+
123+
found << [match.begin(0), :id, "#{match[1]}-#{match[2]}"]
124+
end
125+
found.sort_by.with_index { |(position, _kind, _value), index| [position, index] }
126+
.map { |_position, kind, value| [kind, value] }
127+
end
128+
129+
private
130+
131+
def chapter_ids(repo)
132+
repo.tracked('docs/product-spec/*.md').to_h do |path|
133+
[File.basename(path), repo.read(path).scan(ID).map { |prefix, n| "#{prefix}-#{n}" }.uniq]
134+
end
135+
end
136+
137+
def documents(repo)
138+
repo.tracked(*SCANNED).select { |path| path.end_with?('.md') }
139+
.reject { |path| SKIPPED.match?(path) || EXEMPT_DOCUMENTS.match?(path) }
140+
end
141+
142+
def scan(path, lines, carried)
143+
lines.each_with_index.flat_map do |line, index|
144+
next [] if NEGATION.match?(line + lines.fetch(index + 1, ''))
145+
146+
self.class.pairs(line).filter_map do |chapter, id|
147+
next if chapter == EXEMPT_TARGET || !carried.key?(chapter) || carried[chapter].include?(id)
148+
149+
act(path, index + 1, "attributes #{id} to docs/product-spec/#{chapter}, which does not " \
150+
"carry it (gaps: #{GAPS.join(', ')})")
151+
end
152+
end
153+
end
154+
end
155+
end
156+
end

‎.claude/skills/housekeeping/probe.rb‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ def strip_inline_code(text)
202202
end
203203
end
204204

205-
# The eight read-only checks. Each is a class with `name` and `run(repo) -> [Finding]`.
205+
# The nine read-only checks. Each is a class with `name` and `run(repo) -> [Finding]`. The
206+
# ninth, `chapters`, lives in chapters.rb (phase 10): one concern per file, as guard.rb.
206207
module Checks
207208
# Base class: supplies `name` from the subclass's `NAME` and a `Finding` factory.
208209
class Check
@@ -732,11 +733,14 @@ def symlinks(repo)
732733
end
733734
end
734735

736+
require_relative 'chapters'
737+
735738
# Runs the checks and collects their findings.
736739
class Probe
737740
ALL = [
738741
Checks::Inbox, Checks::Root, Checks::Claims, Checks::Readmes,
739-
Checks::Links, Checks::Registers, Checks::Citations, Checks::GuardCheck
742+
Checks::Links, Checks::Registers, Checks::Citations, Checks::GuardCheck,
743+
Checks::Chapters
740744
].freeze
741745
NAMES = ALL.map { |check| check::NAME }.freeze
742746

‎.claude/skills/housekeeping/test/probe_test.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def test_a_clean_fixture_reports_nothing_on_every_check
5050
assert_empty on_fixture
5151
end
5252

53-
def test_check_names_are_the_eight_the_documentation_states
54-
assert_equal %w[inbox root claims readmes links registers citations guard], Probe::NAMES
53+
def test_check_names_are_the_nine_the_documentation_states
54+
assert_equal %w[inbox root claims readmes links registers citations guard chapters], Probe::NAMES
5555
end
5656

5757
# --- numerals --------------------------------------------------------------------------

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- run: bundle exec rake gates:cause_walk
3333
- run: bundle exec rake gates:bounded_map
3434
- run: bundle exec rake gates:seam_names
35+
- run: bundle exec rake gates:ledger_audit
36+
- run: bundle exec rake gates:spdx_rbs
37+
- run: bundle exec rake gates:sole_parse
3538
- run: bundle exec rake gates:rbs_surface
3639
- run: bundle exec rake gates:sig_diff
3740
- run: bundle exec rake gates:surface_snapshot

‎Rakefile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ DEFAULT_GATES = %w[
2727
gates:cause_walk
2828
gates:bounded_map
2929
gates:seam_names
30+
gates:ledger_audit
31+
gates:spdx_rbs
32+
gates:sole_parse
3033
gates:clean_bundle
3134
gates:rbs_surface
3235
gates:sig_diff

0 commit comments

Comments
 (0)