specpro-cli 0.1.0__py3-none-any.whl

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.
Files changed (76) hide show
  1. specpro_cli/__init__.py +16 -0
  2. specpro_cli/assets/commands/specpro.analyze.md +1102 -0
  3. specpro_cli/assets/commands/specpro.checklist.md +335 -0
  4. specpro_cli/assets/commands/specpro.clarify.md +581 -0
  5. specpro_cli/assets/commands/specpro.constitution.md +488 -0
  6. specpro_cli/assets/commands/specpro.feature.md +115 -0
  7. specpro_cli/assets/commands/specpro.implement.md +1881 -0
  8. specpro_cli/assets/commands/specpro.manual-test.md +206 -0
  9. specpro_cli/assets/commands/specpro.plan.md +3284 -0
  10. specpro_cli/assets/commands/specpro.qc.md +1489 -0
  11. specpro_cli/assets/commands/specpro.scenarios.md +154 -0
  12. specpro_cli/assets/commands/specpro.specify.md +1449 -0
  13. specpro_cli/assets/commands/specpro.status.md +863 -0
  14. specpro_cli/assets/commands/specpro.tasks.md +1207 -0
  15. specpro_cli/assets/commands/specpro.test-implement.md +462 -0
  16. specpro_cli/assets/commands/specpro.test-plan.md +383 -0
  17. specpro_cli/assets/commands/specpro.user-manual.md +178 -0
  18. specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
  19. specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
  20. specpro_cli/assets/scripts/bash/common.sh +88 -0
  21. specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
  22. specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
  23. specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
  24. specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
  25. specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
  26. specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
  27. specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
  28. specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
  29. specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
  30. specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
  31. specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
  32. specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
  33. specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
  34. specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
  35. specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
  36. specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
  37. specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
  38. specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
  39. specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
  40. specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
  41. specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
  42. specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
  43. specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
  44. specpro_cli/assets/templates/agent-context-template.md +49 -0
  45. specpro_cli/assets/templates/assumptions-template.md +248 -0
  46. specpro_cli/assets/templates/checklist-template.md +40 -0
  47. specpro_cli/assets/templates/clarifications-template.md +155 -0
  48. specpro_cli/assets/templates/constitution-template.md +50 -0
  49. specpro_cli/assets/templates/feature-spec-template.md +66 -0
  50. specpro_cli/assets/templates/plan-overview-template.md +150 -0
  51. specpro_cli/assets/templates/plan-template.md +387 -0
  52. specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
  53. specpro_cli/assets/templates/requirements-template.md +356 -0
  54. specpro_cli/assets/templates/spec-template.md +267 -0
  55. specpro_cli/assets/templates/tasks-template.md +252 -0
  56. specpro_cli/assets/templates/test-tasks-template.md +174 -0
  57. specpro_cli/cli/__init__.py +5 -0
  58. specpro_cli/cli/cmd_init.py +416 -0
  59. specpro_cli/cli/cmd_remove.py +122 -0
  60. specpro_cli/cli/entry.py +181 -0
  61. specpro_cli/integrations/__init__.py +36 -0
  62. specpro_cli/integrations/base.py +601 -0
  63. specpro_cli/integrations/claude/__init__.py +101 -0
  64. specpro_cli/integrations/copilot/__init__.py +153 -0
  65. specpro_cli/integrations/cursor_agent/__init__.py +51 -0
  66. specpro_cli/integrations/gemini/__init__.py +44 -0
  67. specpro_cli/integrations/opencode/__init__.py +48 -0
  68. specpro_cli/integrations/qodercli/__init__.py +54 -0
  69. specpro_cli/integrations/registry.py +88 -0
  70. specpro_cli/packaged/__init__.py +5 -0
  71. specpro_cli/packaged/sync.py +106 -0
  72. specpro_cli-0.1.0.dist-info/METADATA +117 -0
  73. specpro_cli-0.1.0.dist-info/RECORD +76 -0
  74. specpro_cli-0.1.0.dist-info/WHEEL +4 -0
  75. specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
  76. specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,978 @@
1
+ #!/usr/bin/env pwsh
2
+ #
3
+ # Shape-detection verifier — PowerShell twin of scripts/bash/verify-shapes.sh.
4
+ #
5
+ # Same stdout, same exit code, by the contract in `specs/contracts/dev-shellcheck-tool.md`
6
+ # and FR-008. The rationale lives in the bash side's header; keep the two in step.
7
+ #
8
+ # ⚠️ ONE DECLARED NON-PARITY, stated here rather than discovered later — 落点政策 §3:
9
+ # shapes 1 and 6 EXECUTE a criterion that is carried in a document as **bash**, and the
10
+ # only way to run a bash function is with bash. Both sides therefore run the criterion
11
+ # through a `bash -c` subprocess, and on a host WITHOUT bash this side reports those two
12
+ # shapes as `⊘ not executable` instead of guessing a verdict. That is a difference in the
13
+ # verdict by design: the alternative is a second implementation of the criterion in
14
+ # PowerShell, which would be a second source (形态 3) and would drift from the first.
15
+ #
16
+ # ⚠️ `.Contains` / `[regex]` stand in for `grep -qF` / `grep -E`; POSIX classes have no
17
+ # .NET equivalent, so `[[:space:]]` becomes `[ \t]`. Both substitutions are visible at
18
+ # every use site, and the parity suite is what proves the two agree.
19
+
20
+ $Base = if ($env:SHAPES_HOOK_BASE) { $env:SHAPES_HOOK_BASE }
21
+ else { (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path }
22
+
23
+ $script:Hits = 0
24
+ $script:Oks = 0
25
+ $script:Skips = 0
26
+
27
+ function Write-Hit { param([string]$m) $script:Hits++; Write-Output " ✗ $m" }
28
+ function Write-Ok { param([string]$m) $script:Oks++; Write-Output " ✓ $m" }
29
+ function Write-Skip { param([string]$m) $script:Skips++; Write-Output " ⊘ $m" }
30
+
31
+ # ---------------------------------------------------------------------------------------
32
+ # ⊘ sites that are a LIVE SILENCE — 形态 12's subject, collected as DATA
33
+ # ---------------------------------------------------------------------------------------
34
+ # `Write-Skip` above means "this subject could not be exercised **here**" (absent carrier,
35
+ # stale registry row, empty registry). `Write-Unreceived` means something stronger: **the
36
+ # subject exists and was not judged**, so somebody must own filling its scope.
37
+ #
38
+ # ⚠️ Why the receiver is a COLLECTED PAIR and not parsed back out of the report text
39
+ # (`T213` / `ISS-177`): deriving the subject from the message is 形态 1 — a semantic
40
+ # conclusion drawn from a literal match — and it would go silently dead the first time
41
+ # somebody reworded a message. The messages below are NOT uniform (some carry a subject id,
42
+ # some do not), so there is nothing stable to parse even today.
43
+ $script:Unreceived = @()
44
+ function Write-Unreceived {
45
+ param([string]$Receiver, [string]$Message)
46
+ $script:Skips++
47
+ $script:Unreceived += ,@($Receiver, $Message)
48
+ Write-Output " ⊘ $Message"
49
+ }
50
+
51
+ # Is <id> present as an ENTRY — not as a mention? The pattern is the entry body's
52
+ # line-start shape, which is the discipline the ledger's own verifier already uses
53
+ # (`^- \[[x ]\] ISS-<N>:` rather than a full-text ID search; `TOOL-009`). A full-text
54
+ # match would count every cross-reference as a receiver, and this file's own commentary
55
+ # names these ids — so the weaker check would pass on its own prose.
56
+ function Test-ReceiversExist {
57
+ param([string]$Id)
58
+ foreach ($f in @("specs/tasks.md", "specs/implement_issues.md")) {
59
+ $path = Join-Path $Base $f
60
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
61
+ if (Get-Count -Path $path -Pattern ("^- \[[x ]\] " + [regex]::Escape($Id) + "([ :]|$)")) {
62
+ return $true
63
+ }
64
+ }
65
+ return $false
66
+ }
67
+
68
+ function Get-Lines {
69
+ param([string]$Path)
70
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return @() }
71
+ $text = [System.IO.File]::ReadAllText($Path) -replace "`r`n", "`n"
72
+ # ⚠️ No `,` around the result: `return ,$array` wraps the array in a one-element array,
73
+ # and every caller feeds it straight into a pipeline — which then enumerates ONE item
74
+ # (the whole file) instead of one item per line. That single character made shapes 3, 4
75
+ # and 7 read the entire document as a single line.
76
+ return ($text -split "`n")
77
+ }
78
+
79
+ function Get-MatchLines {
80
+ param([string]$Path, [string]$Pattern)
81
+ return @(Get-Lines $Path | Where-Object { $_ -match $Pattern })
82
+ }
83
+
84
+ function Get-Count {
85
+ param([string]$Path, [string]$Pattern)
86
+ return @(Get-MatchLines $Path $Pattern).Count
87
+ }
88
+
89
+ function Get-CountUnfenced {
90
+ # ⚠️ **只数围栏之外的行** —— 与 Bash 侧 `count_of_unfenced` 同一理由:模板里有一条
91
+ # `- [ ] **Status**: ⊘ Skipped` 的 fenced EXAMPLE,逐行计数会把它算成第 12 项。
92
+ param([string]$Path, [string]$Pattern)
93
+ $n = 0; $inFence = $false
94
+ foreach ($ln in (Get-Lines $Path)) {
95
+ if ($ln -match '^[ ]*```') { $inFence = -not $inFence; continue }
96
+ if ((-not $inFence) -and ($ln -match $Pattern)) { $n++ }
97
+ }
98
+ return $n
99
+ }
100
+
101
+ function Test-HasMatch {
102
+ param([string]$Path, [string]$Pattern)
103
+ return (@(Get-MatchLines $Path $Pattern).Count -gt 0)
104
+ }
105
+
106
+ function Get-FullText {
107
+ param([string]$Path)
108
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return "" }
109
+ return ([System.IO.File]::ReadAllText($Path) -replace "`r`n", "`n")
110
+ }
111
+
112
+ function Get-FirstNumber {
113
+ param([string]$Path, [string]$Pattern)
114
+ # Mirrors `grep -oE "$Pattern" file | grep -oE '[0-9]+' | head -1`: the digits are taken
115
+ # from the MATCHED SUBSTRING, not from its line. Taking them from the line picks up an
116
+ # earlier number that has nothing to do with the claim — a file stating both "12
117
+ # templates" and "15 channels" on one line reads as 12 for the channel count.
118
+ $m = [regex]::Match((Get-FullText $Path), $Pattern)
119
+ if (-not $m.Success) { return "" }
120
+ $d = [regex]::Match($m.Value, '[0-9]+')
121
+ if ($d.Success) { return $d.Value }
122
+ return ""
123
+ }
124
+
125
+ # --- the criterion harness, shared by shapes 1 and 6 -----------------------------------
126
+ # ⚠️ The criterion is EXTRACTED from its carrier, never re-stated here: a second copy of
127
+ # the predicate is a second source (形态 3). Extraction that silently returns nothing is
128
+ # ISS-83's shape, so the harness reports `not executable` rather than a verdict.
129
+ function Get-Fence {
130
+ param([string]$Carrier, [string]$Fn)
131
+ if (-not (Test-Path -LiteralPath $Carrier -PathType Leaf)) { return "" }
132
+ $buf = ""
133
+ $inFence = $false
134
+ foreach ($line in (Get-Lines $Carrier)) {
135
+ if ($line -match '^```bash') { $inFence = $true; $buf = ""; continue }
136
+ if ($line -match '^```') {
137
+ if ($inFence -and $buf -match ([regex]::Escape($Fn) + '\(\)')) { return $buf }
138
+ $inFence = $false; continue
139
+ }
140
+ if ($inFence) { $buf += $line + "`n" }
141
+ }
142
+ return ""
143
+ }
144
+
145
+ function Get-Probe {
146
+ param([string]$Cid, [string]$Kind)
147
+ $key = "$Cid-$Kind"
148
+ switch ($key) {
149
+ 'CQ001-compliant' {
150
+ @'
151
+ ## Requirements *(mandatory)*
152
+
153
+ ### Functional Requirements
154
+
155
+ - **FR-001**: The system MUST classify incoming events into categories.
156
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
157
+ System MUST classify each event and record its category.
158
+
159
+ - **FR-002**: The system MUST show a definition of every category it reports.
160
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
161
+ System MUST present a declarative summary for each category.
162
+ '@
163
+ }
164
+ 'CQ001-falsifying' {
165
+ @'
166
+ ## Requirements *(mandatory)*
167
+
168
+ ### Functional Requirements
169
+
170
+ - **FR-001**: The system MUST render its configuration screen with React.
171
+ **Lifecycle**: [specify:create][plan:create][tasks:create]
172
+ System MUST use an async/await HTTP client for the sync endpoint.
173
+ '@
174
+ }
175
+ 'CS003-compliant' {
176
+ @'
177
+ ### US1 - Alpha (Priority: P1)
178
+
179
+ **Independent Test**: run it.
180
+
181
+ ### US2 - Beta (Priority: P2)
182
+
183
+ **Independent Test**: run it.
184
+
185
+ ## Requirements *(mandatory)*
186
+
187
+ #### Alpha (US1)
188
+
189
+ - **FR-001**: The system MUST do alpha.
190
+
191
+ #### Beta (US2)
192
+
193
+ - **FR-002**: The system MUST do beta.
194
+ '@
195
+ }
196
+ 'CS003-falsifying' {
197
+ @'
198
+ ### US1 - Alpha (Priority: P1)
199
+
200
+ **Independent Test**: run it.
201
+
202
+ ### US2 - Beta (Priority: P2)
203
+
204
+ **Independent Test**: run it.
205
+
206
+ ## Requirements *(mandatory)*
207
+
208
+ #### Alpha (US1)
209
+
210
+ - **FR-001**: The system MUST do alpha.
211
+ '@
212
+ }
213
+ # ---- the rest (T192, minus one retired 2026-09-19 / T197) — same as bash --------
214
+ # Each is a MINIMAL input that falsifies its criterion's predicate. They are the
215
+ # cases T124 ran through a throwaway harness; without a home here they had no
216
+ # owner at all (ISS-151). ⚠️ The probe for the retired criterion was removed with
217
+ # it: a probe for a criterion that no longer exists would be registered against a
218
+ # `CRITERIA` row that no longer exists, and `S6_DECLARED` would then never reach it.
219
+ 'CQ003-falsifying' {
220
+ @'
221
+ ## Requirements
222
+
223
+ - **FR-001**: The system MUST implement search using React for the index.
224
+ '@
225
+ }
226
+ 'CP001-falsifying' {
227
+ @'
228
+ ## Requirements *(mandatory)*
229
+
230
+ ### Functional Requirements
231
+
232
+ - **FR-001**: The system MUST do the thing.
233
+ '@
234
+ }
235
+ 'CP002-falsifying' {
236
+ @'
237
+ ### US1 - Alpha (Priority: P1)
238
+
239
+ No scenarios here at all.
240
+
241
+ ### US2 - Beta (Priority: P2)
242
+
243
+ **Acceptance Scenarios**:
244
+
245
+ 1. **Given** a, **When** b, **Then** c
246
+ '@
247
+ }
248
+ 'CP003-falsifying' {
249
+ @'
250
+ ### US1 - Alpha (Priority: P1)
251
+
252
+ **Acceptance Scenarios**:
253
+
254
+ 1. **Given** a live session, **When** the screen changes, **Then** it updates within 200ms
255
+
256
+ **Why this priority**: because.
257
+ '@
258
+ }
259
+ 'CS001-falsifying' {
260
+ @'
261
+ ### US1 - Alpha (Priority: P1)
262
+
263
+ The system MUST support offline mode for disconnected sites.
264
+
265
+ ### US2 - Beta (Priority: P2)
266
+
267
+ The system MUST require an always-on connection to the server.
268
+ '@
269
+ }
270
+ 'CS002-falsifying' {
271
+ @'
272
+ ## Requirements
273
+
274
+ - **FR-001**: The system MUST store the data locally on the device.
275
+ - **FR-002**: The system MUST NOT use local storage for records.
276
+ '@
277
+ }
278
+ # ⚠️ **Why the cited id is `FR-042` and not a number outside the project's range** (T202),
279
+ # same as the bash twin: the probe's subject is "cited in this sample but not
280
+ # defined in it", and any number satisfies that — while one ABOVE the project's
281
+ # range is indistinguishable from a foreign identifier to the anti-coupling scan's
282
+ # class 1, turning the fixture itself into an O1 finding. An id inside the project's
283
+ # range removes the false finding without touching the probe or the criterion.
284
+ 'CS004-falsifying' {
285
+ @'
286
+ ## Requirements
287
+
288
+ - **FR-001**: The system MUST do alpha, per FR-042.
289
+ '@
290
+ }
291
+ 'TR001-falsifying' {
292
+ @'
293
+ ## Requirements *(mandatory)*
294
+
295
+ - **FR-001**: The system MUST do the thing.
296
+ '@
297
+ }
298
+ 'TR002-falsifying' {
299
+ @'
300
+ ## Requirements *(mandatory)*
301
+
302
+ - **FR-001**: The system MUST call the external service to settle the order.
303
+ '@
304
+ }
305
+ default { return $null }
306
+ }
307
+ }
308
+
309
+ $Criteria = @(
310
+ "CQ001|commands/specpro.qc.md|check_cq001_implementation_details",
311
+ "CQ003|commands/specpro.qc.md|check_cq003_functional_not_technical",
312
+ "CP001|commands/specpro.qc.md|check_cp001_mandatory_sections",
313
+ "CP002|commands/specpro.qc.md|check_cp002_acceptance_scenarios",
314
+ "CP003|commands/specpro.qc.md|check_cp003_scenarios_functional",
315
+ "CS001|commands/specpro.qc.md|check_cs001_us_consistency",
316
+ "CS002|commands/specpro.qc.md|check_cs002_fr_consistency",
317
+ "CS003|commands/specpro.qc.md|check_cs003_us_fr_mapping",
318
+ "CS004|commands/specpro.qc.md|check_cs004_reference_integrity",
319
+ "TR001|commands/specpro.qc.md|check_tr001_constitution_constraints",
320
+ "TR002|commands/specpro.qc.md|check_tr002_external_dependencies"
321
+ )
322
+
323
+ function Invoke-Criterion {
324
+ param([string]$Cid, [string]$Carrier, [string]$Fn, [string]$Kind)
325
+ $probe = Get-Probe -Cid $Cid -Kind $Kind
326
+ if ($null -eq $probe) { return $null }
327
+ $block = Get-Fence -Carrier (Join-Path $Base $Carrier) -Fn $Fn
328
+ if ([string]::IsNullOrEmpty($block)) { return $null }
329
+ $bash = Get-Command bash -ErrorAction SilentlyContinue
330
+ if (-not $bash) { return $null }
331
+
332
+ $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
333
+ [System.IO.File]::WriteAllText($tmp, ($probe -replace "`r`n", "`n") + "`n")
334
+ # ⚠️ The working directory and an ABSOLUTE template path are both load-bearing: CP001
335
+ # and TR002 read `$SPEC_TEMPLATE` (unset ⇒ "" ⇒ "the template declares no mandatory
336
+ # sections"), and TR001 reads `specs/constitution.md` relative to the working directory.
337
+ # Without these, the harness reports three criteria as wrong for reasons that have
338
+ # nothing to do with the criteria.
339
+ $env:FEATURE_SPEC = $tmp
340
+ $env:TARGET_FRS = "all"
341
+ $env:SCOPE = "full"
342
+ $env:SPEC_TEMPLATE = (Join-Path $Base ".specpro/templates/spec-template.md")
343
+ $driver = @'
344
+ eval "$1"
345
+ PASSED=0; FAILED=0; SKIPPED=0; FAILED_ITEMS=(); AUTO_FIXED_COUNT=0
346
+ NEED_CLARIFY=false
347
+ auto_fix_implementation_details() { return 0; }
348
+ add_to_clarifications() { return 0; }
349
+ update_checklist() { return 0; }
350
+ save_qc_cache() { return 0; }
351
+ "$2" 2>/dev/null
352
+ '@
353
+ Push-Location $Base
354
+ try { $out = & bash -c $driver _ $block $Fn 2>$null }
355
+ finally { Pop-Location }
356
+ Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
357
+ return (@($out) -join "`n")
358
+ }
359
+
360
+ Write-Output "Shape detections: $Base"
361
+ Write-Output ""
362
+
363
+ # =======================================================================================
364
+ # SHAPE 1 — literal match used as a semantic conclusion
365
+ # =======================================================================================
366
+ Write-Output " [1] literal match used as a semantic conclusion"
367
+ $s1N = 0
368
+ $s1Unprobed = @()
369
+ foreach ($row in $Criteria) {
370
+ $parts = $row -split '\|'
371
+ $cid = $parts[0]; $carrier = $parts[1]; $fn = $parts[2]
372
+ $s1N++
373
+ $verdict = Invoke-Criterion -Cid $cid -Carrier $carrier -Fn $fn -Kind "compliant"
374
+ if ($null -eq $verdict) {
375
+ # The criterion is reachable; the COMPLIANT PROBE is what is missing. Reporting
376
+ # those as "not reachable" was untrue, and a ⊘ with the wrong reason costs more
377
+ # than no line — the reader checks the claim, finds the function, and stops
378
+ # trusting the report. They are aggregated once, as a coverage fact.
379
+ $s1Unprobed += $cid
380
+ continue
381
+ }
382
+ if ($verdict -match '✗') {
383
+ Write-Hit "SHAPE-1 $cid`: reports on a compliant probe where the keywords are substrings only"
384
+ foreach ($l in (@($verdict -split "`n") | Where-Object { $_ -match '✗' } | Select-Object -First 2)) {
385
+ Write-Output (" " + $l)
386
+ }
387
+ } else {
388
+ Write-Ok "SHAPE-1 $cid`: compliant probe reported clean"
389
+ }
390
+ }
391
+ if (@($s1Unprobed).Count -gt 0) {
392
+ Write-Unreceived "T124" "SHAPE-1: $(@($s1Unprobed).Count) of $s1N criteria have no COMPLIANT probe — unproven, not clean: $($s1Unprobed -join ' ')"
393
+ }
394
+ if ($s1N -eq 0) { Write-Skip "SHAPE-1: no criterion is registered" }
395
+
396
+ # =======================================================================================
397
+ # SHAPE 2 — mechanism written but not wired into the execution chain
398
+ # =======================================================================================
399
+ Write-Output " [2] mechanism written but not wired into the execution chain"
400
+ $S2Claims = @(
401
+ "verify-ledger-enforcement|commands/specpro.implement.md|verify-ledger|scripts/git-hooks/pre-commit|verify-ledger",
402
+ "verify-deployment-enforcement|CLAUDE.md|verify-deployment|scripts/git-hooks/pre-commit|verify-deployment"
403
+ )
404
+ $s2N = 0
405
+ foreach ($row in $S2Claims) {
406
+ $p = $row -split '\|'
407
+ $cid = $p[0]; $claimFile = $p[1]; $subject = $p[2]; $enforcer = $p[3]; $inv = $p[4]
408
+ if ([string]::IsNullOrEmpty($cid)) { continue }
409
+ $s2N++
410
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $claimFile) -PathType Leaf)) {
411
+ Write-Skip "SHAPE-2 $cid`: not executable — claim carrier $claimFile is absent"
412
+ continue
413
+ }
414
+ if (-not (Test-HasMatch -Path (Join-Path $Base $claimFile) -Pattern ([regex]::Escape($subject)))) {
415
+ Write-Skip "SHAPE-2 $cid`: the claim is no longer made in $claimFile (registry row is stale)"
416
+ continue
417
+ }
418
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $enforcer) -PathType Leaf)) {
419
+ Write-Hit "SHAPE-2 $cid`: $claimFile claims enforcement by $enforcer, which does not exist"
420
+ continue
421
+ }
422
+ if (Test-HasMatch -Path (Join-Path $Base $enforcer) -Pattern ([regex]::Escape($inv))) {
423
+ Write-Ok "SHAPE-2 $cid`: $enforcer really invokes '$inv'"
424
+ } else {
425
+ Write-Hit "SHAPE-2 $cid`: $enforcer does not invoke '$inv' — the claim has no enforcement point"
426
+ }
427
+ }
428
+ if ($s2N -eq 0) { Write-Skip "SHAPE-2: no enforcement claim is registered" }
429
+
430
+ # --- the consumer side: is a consumer NAMED where the artifact is defined? ---------------
431
+ # 判据 (Q2's OTHER half — `ISS-174` / `T212`): 宪法 Q2 要求「新增产物时必须**同时指定**其负责
432
+ # 命令与**具名消费者**」。⚠️ **写入侧**有强制点(上面那两行:pre-commit 真的调用核验体);
433
+ # **消费者侧此前什么都没有** —— 而 `S2Claims` 问的是另一个问题("声称的强制者是否真的调用
434
+ # 它"),把两者塞进同一个表会让"命令有没有声明该产物"与"产物有没有人读"互相冒充。
435
+ #
436
+ # ⚠️ 注册的对象是「**产物 ↔ 它的定义载体**」这一**对**,不是产物单侧。单侧那一半
437
+ # ("这个字在仓库里出现过吗")是**形态 1**(字面匹配求语义结论)—— 而本文件的 SHAPE 1
438
+ # 存在的意义就是查它。
439
+ #
440
+ # ⚠️ 今天每一行都是 `carrier == artifact`(这些产物**在自身**里点名消费者)。该列**不是装饰**:
441
+ # 它是"消费者被写在**契约**里而不是产物里"那一行要用的槽位,而这样的行**今天还没有**。
442
+ #
443
+ # ⚠️ **一次命中证明了什么、没证明什么**:载体里有 ≥1 行匹配,回答的是「**这里点名了消费者**」;
444
+ # **不**回答「这个产物的每一部分都有」—— 后者是**形态 10**(一般规则只以实例形式存在)的
445
+ # 判据,是另一处检查。写在这里,是因为把它当成后者读的人**会停止寻找**。
446
+ $S2ConsumerRows = @(
447
+ "ledger-sections|specs/contracts/ledger.md|specs/contracts/ledger.md|^## Sections and their consumers",
448
+ "plan-purpose-consumers|specs/plan.md|specs/plan.md|^\*Purpose:.*Consumers:"
449
+ )
450
+ $s2cN = 0
451
+ foreach ($row in $S2ConsumerRows) {
452
+ $p = $row -split '\|'
453
+ $cid = $p[0]; $artifact = $p[1]; $carrier = $p[2]; $cpattern = $p[3]
454
+ if ([string]::IsNullOrEmpty($cid)) { continue }
455
+ $s2cN++
456
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $artifact) -PathType Leaf)) {
457
+ Write-Skip "SHAPE-2 $cid`: not executable — the artifact $artifact is absent"
458
+ continue
459
+ }
460
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $carrier) -PathType Leaf)) {
461
+ Write-Hit "SHAPE-2 $cid`: $artifact names no consumer — its definition carrier $carrier does not exist"
462
+ continue
463
+ }
464
+ if ((Get-Count -Path (Join-Path $Base $carrier) -Pattern $cpattern) -gt 0) {
465
+ Write-Ok "SHAPE-2 $cid`: $artifact names its consumer(s) in $carrier"
466
+ } else {
467
+ Write-Hit "SHAPE-2 $cid`: $artifact names no consumer in $carrier — Q2 requires one"
468
+ }
469
+ }
470
+ if ($s2cN -eq 0) { Write-Skip "SHAPE-2: no consumer-side row is registered (Q2's consumer half is unproven, not clean)" }
471
+
472
+ # =======================================================================================
473
+ # SHAPE 3 — the same fact in two places, and they disagree
474
+ # =======================================================================================
475
+ Write-Output " [3] the same fact in two places, and they disagree"
476
+ $S3Facts = @(
477
+ "analyze-pass-count|commands/specpro.analyze.md|^#### ([A-P])\. |count|CLAUDE.md|([0-9]+) 个检测通道"
478
+ )
479
+ $s3N = 0
480
+ foreach ($row in $S3Facts) {
481
+ $p = $row -split '\|'
482
+ $fid = $p[0]; $aFile = $p[1]; $aRe = $p[2]; $arity = $p[3]; $bFile = $p[4]; $bRe = $p[5]
483
+ if ([string]::IsNullOrEmpty($fid)) { continue }
484
+ $s3N++
485
+ $aPath = Join-Path $Base $aFile; $bPath = Join-Path $Base $bFile
486
+ if (-not (Test-Path -LiteralPath $aPath -PathType Leaf) -or -not (Test-Path -LiteralPath $bPath -PathType Leaf)) {
487
+ Write-Skip "SHAPE-3 $fid`: not executable — a carrier is absent"
488
+ continue
489
+ }
490
+ if ($arity -eq "count") {
491
+ $av = [string](Get-Count -Path $aPath -Pattern $aRe)
492
+ } else {
493
+ $m = @(Get-MatchLines -Path $aPath -Pattern $aRe)
494
+ $av = if (@($m).Count -gt 0) { $m[0] } else { "" }
495
+ }
496
+ $bv = Get-FirstNumber -Path $bPath -Pattern $bRe
497
+ if ([string]::IsNullOrEmpty($av) -or [string]::IsNullOrEmpty($bv)) {
498
+ Write-Skip "SHAPE-3 $fid`: not executable — one side states no value (a form change, not agreement)"
499
+ continue
500
+ }
501
+ if ($av -eq $bv) {
502
+ Write-Ok "SHAPE-3 $fid`: both carriers state $av"
503
+ } else {
504
+ Write-Hit "SHAPE-3 $fid`: $aFile says $av, $bFile says $bv"
505
+ }
506
+ }
507
+ if ($s3N -eq 0) { Write-Skip "SHAPE-3: no fact pair is registered" }
508
+
509
+ # =======================================================================================
510
+ # SHAPE 4 — the report disagrees with what is actually there
511
+ # =======================================================================================
512
+ Write-Output " [4] the report disagrees with what is actually there"
513
+ $S4Rows = @(
514
+ "checklist-total|specs/checklists/requirements.md|Total Items|\*\*Total Items\*\*: ([0-9]+)|unfenced:^[ \t]*- \[[ x]\] \*\*Status\*\*:[ \t]*[^ \t]+",
515
+ "checklist-passed|specs/checklists/requirements.md|Passed|\*\*Passed\*\*: ([0-9]+)|unfenced:^[ \t]*- \[[ x]\] \*\*Status\*\*:[ \t]*x[ \t]*$",
516
+ "checklist-failed|specs/checklists/requirements.md|Failed|\*\*Failed\*\*: ([0-9]+)|unfenced:^[ \t]*- \[[ x]\] \*\*Status\*\*:[ \t]*✗",
517
+ "req-template-total|templates/requirements-template.md|Total Items|\*\*Total Items\*\*: ([0-9]+)|^### [A-Z]{2}[0-9]{3}:"
518
+ )
519
+ $s4N = 0
520
+ foreach ($row in $S4Rows) {
521
+ $p = $row -split '\|'
522
+ $rid = $p[0]; $rFile = $p[1]; $label = $p[2]; $claimRe = $p[3]; $countRe = $p[4]
523
+ if ([string]::IsNullOrEmpty($rid)) { continue }
524
+ $s4N++
525
+ $rPath = Join-Path $Base $rFile
526
+ if (-not (Test-Path -LiteralPath $rPath -PathType Leaf)) {
527
+ Write-Skip "SHAPE-4 $rid`: not executable — report $rFile is absent"
528
+ continue
529
+ }
530
+ $stated = Get-FirstNumber -Path $rPath -Pattern $claimRe
531
+ # ⚠️ `unfenced:` 前缀 = 这一行必须跳过围栏内的行(见 Get-CountUnfenced 的理由)
532
+ if ($countRe.StartsWith("unfenced:")) {
533
+ # ⚠️ **9, not 10** — `"unfenced:"`.Length is 9, and off-by-one here is SILENT:
534
+ # dropping one extra character takes `^` off the pattern, after which it matches
535
+ # anywhere and the count goes back to including the fenced example. Measured:
536
+ # re-derived 12 (wrong) with 10, 11 (right) with 9.
537
+ $actual = [string](Get-CountUnfenced -Path $rPath -Pattern $countRe.Substring(9))
538
+ } else {
539
+ $actual = [string](Get-Count -Path $rPath -Pattern $countRe)
540
+ }
541
+ if ([string]::IsNullOrEmpty($stated)) {
542
+ Write-Skip "SHAPE-4 $rid`: not executable — the report states no '$label' value"
543
+ continue
544
+ }
545
+ if ($stated -eq $actual) {
546
+ Write-Ok "SHAPE-4 $rid`: report says $stated, re-derived $actual"
547
+ } else {
548
+ Write-Hit "SHAPE-4 $rid`: report says $label=$stated, re-derived $actual"
549
+ }
550
+ }
551
+ if ($s4N -eq 0) { Write-Skip "SHAPE-4: no report claim is registered" }
552
+
553
+ # =======================================================================================
554
+ # SHAPE 5 — the deliverable is there, the record that receives it is not
555
+ # =======================================================================================
556
+ Write-Output " [5] the deliverable is there, the record that receives it is not"
557
+ $S5Globs = @("scripts/bash/*.sh", "tools/*.sh", "tools/*.md")
558
+ $s5Tasks = Join-Path $Base "specs/tasks.md"
559
+ $s5Ledger = Join-Path $Base "specs/implement_issues.md"
560
+ $s5Corpora = @()
561
+ foreach ($c in @($s5Tasks, $s5Ledger)) { if (Test-Path -LiteralPath $c -PathType Leaf) { $s5Corpora += $c } }
562
+ $s5N = 0
563
+ $s5Files = @()
564
+ foreach ($g in $S5Globs) {
565
+ foreach ($f in @(Get-ChildItem -Path (Join-Path $Base $g) -File -ErrorAction SilentlyContinue)) {
566
+ $s5Files += $f
567
+ }
568
+ }
569
+ if ($s5Corpora.Count -eq 0) {
570
+ Write-Skip "SHAPE-5: not executable — neither tasks.md nor the ledger exists, so nothing can receive anything"
571
+ $s5N = @($s5Files).Count
572
+ } else {
573
+ foreach ($f in $s5Files) {
574
+ $s5N++
575
+ $name = $f.Name
576
+ $found = $false
577
+ foreach ($c in $s5Corpora) {
578
+ if ((Get-FullText $c).Contains($name)) { $found = $true }
579
+ }
580
+ if ($found) {
581
+ Write-Ok "SHAPE-5 $name`: named by a task or a ledger entry"
582
+ } else {
583
+ Write-Hit "SHAPE-5 $name`: no task and no ledger entry receives it"
584
+ }
585
+ }
586
+ }
587
+ if ($s5N -eq 0) { Write-Skip "SHAPE-5: the registered globs matched no artifact — the scope is the finding" }
588
+
589
+ # =======================================================================================
590
+ # SHAPE 6 — the criterion is voided by the way it is written
591
+ # =======================================================================================
592
+ Write-Output " [6] the criterion is voided by the way it is written"
593
+ $s6Carrier = "commands/specpro.qc.md"
594
+ $s6Body = Get-Fence -Carrier (Join-Path $Base $s6Carrier) -Fn "run_qc_check"
595
+ $s6Declared = @()
596
+ if (-not [string]::IsNullOrEmpty($s6Body)) {
597
+ foreach ($l in ($s6Body -split "`n")) {
598
+ $m = [regex]::Match($l, '^ (check_[a-z0-9_]+)')
599
+ if ($m.Success) { $s6Declared += $m.Groups[1].Value }
600
+ }
601
+ $s6Declared = @($s6Declared | Sort-Object -Unique)
602
+ }
603
+ if (@($s6Declared).Count -eq 0) {
604
+ Write-Skip "SHAPE-6: not executable — no criterion is declared in $s6Carrier's run_qc_check"
605
+ } else {
606
+ $unproven = @()
607
+ foreach ($fn in $s6Declared) {
608
+ $m = [regex]::Match($fn, '^check_([a-z]+[0-9]+)_')
609
+ $cid = if ($m.Success) { $m.Groups[1].Value.ToUpper() } else { $fn.ToUpper() }
610
+ $row = @($Criteria | Where-Object { $_ -match ('^' + [regex]::Escape($cid) + '\|') })
611
+ if (@($row).Count -eq 0) { $unproven += $cid; continue }
612
+ $parts = $row[0] -split '\|'
613
+ $verdict = Invoke-Criterion -Cid $cid -Carrier $parts[1] -Fn $parts[2] -Kind "falsifying"
614
+ if ($null -eq $verdict) {
615
+ Write-Skip "SHAPE-6 $cid`: not executable — the falsifying probe or the criterion is unreachable"
616
+ } elseif ($verdict -match '✗') {
617
+ Write-Ok "SHAPE-6 $cid`: judges fail on an input that falsifies it"
618
+ } else {
619
+ Write-Hit "SHAPE-6 $cid`: did NOT judge fail on a falsifying input — always-true, or never ran"
620
+ foreach ($l in (@($verdict -split "`n") | Select-Object -First 2)) {
621
+ Write-Output (" " + $l)
622
+ }
623
+ }
624
+ }
625
+ if (@($unproven).Count -gt 0) {
626
+ $names = ($unproven -join " ")
627
+ Write-Unreceived "T124" "SHAPE-6: $(@($unproven).Count) declared criterion(s) have no falsifying probe — unproven, not clean: $names"
628
+ }
629
+ }
630
+
631
+ # =======================================================================================
632
+ # SHAPE 7 — self-witnessing (the evidence's producer is the judged implementation)
633
+ # =======================================================================================
634
+ Write-Output " [7] self-witnessing (the evidence's producer is the judged implementation)"
635
+ $S7Cite = '^[ \t]*>[ \t]*\**[Ss]ource\**[ \t]*[::]'
636
+ $S7Rows = @(
637
+ "dev-shellcheck-contract|specs/contracts/dev-shellcheck-tool.md|tools/verify-doc-shell\.sh",
638
+ "ledger-contract|specs/contracts/ledger.md|scripts/bash/verify-ledger\.sh"
639
+ )
640
+ $s7N = 0
641
+ foreach ($row in $S7Rows) {
642
+ $p = $row -split '\|'
643
+ $aid = $p[0]; $aFile = $p[1]; $subject = $p[2]
644
+ if ([string]::IsNullOrEmpty($aid)) { continue }
645
+ $s7N++
646
+ $aPath = Join-Path $Base $aFile
647
+ if (-not (Test-Path -LiteralPath $aPath -PathType Leaf)) {
648
+ Write-Skip "SHAPE-7 $aid`: not executable — artifact $aFile is absent"
649
+ continue
650
+ }
651
+ $cites = Get-Count -Path $aPath -Pattern $S7Cite
652
+ if ($cites -eq 0) {
653
+ Write-Unreceived "ISS-178" "SHAPE-7 $aid`: not executable — $aFile carries no Source: citation, so there is no evidence chain to audit"
654
+ continue
655
+ }
656
+ $selfwitness = @(Get-MatchLines -Path $aPath -Pattern $S7Cite | Where-Object { $_ -match $subject })
657
+ if (@($selfwitness).Count -gt 0) {
658
+ Write-Hit "SHAPE-7 $aid`: a Source: line attributes the fact to the surface being judged"
659
+ $line = $selfwitness[0]
660
+ if ($line.Length -gt 120) { $line = $line.Substring(0, 120) }
661
+ Write-Output (" " + $line)
662
+ } else {
663
+ Write-Ok "SHAPE-7 $aid`: $cites Source: citation(s), none of them self-witnessing"
664
+ }
665
+ }
666
+ if ($s7N -eq 0) { Write-Skip "SHAPE-7: no artifact is registered" }
667
+
668
+ # =======================================================================================
669
+ # SHAPE 8 — position decides ownership and there is no boundary sentinel
670
+ # =======================================================================================
671
+ Write-Output " [8] position decides ownership and there is no boundary sentinel"
672
+ $S8Rows = @(
673
+ "ledger|specs/implement_issues.md|^-->$",
674
+ "requirements-checklist|specs/checklists/requirements.md|^[ \t]*<!--[ \t]*[Ee]nd-of-file sentinel"
675
+ )
676
+ $s8N = 0
677
+ foreach ($row in $S8Rows) {
678
+ $p = $row -split '\|'
679
+ $sid = $p[0]; $sFile = $p[1]; $marker = $p[2]
680
+ if ([string]::IsNullOrEmpty($sid)) { continue }
681
+ $s8N++
682
+ $sPath = Join-Path $Base $sFile
683
+ if (-not (Test-Path -LiteralPath $sPath -PathType Leaf)) {
684
+ Write-Skip "SHAPE-8 $sid`: not executable — carrier $sFile is absent"
685
+ continue
686
+ }
687
+ if (Test-HasMatch -Path $sPath -Pattern $marker) {
688
+ Write-Ok "SHAPE-8 $sid`: $sFile declares a boundary sentinel"
689
+ } else {
690
+ Write-Hit "SHAPE-8 $sid`: $sFile owns rows by position and declares no boundary sentinel"
691
+ }
692
+ }
693
+ if ($s8N -eq 0) { Write-Skip "SHAPE-8: no position-owned structure is registered" }
694
+
695
+ # =======================================================================================
696
+ # SHAPE 9 — a compatibility layer for a gap that is not there
697
+ # =======================================================================================
698
+ Write-Output " [9] a compatibility layer for a gap that is not there"
699
+ $S9Rows = @(
700
+ "create-new-feature|scripts/bash/create-new-feature.sh|commands/specpro.specify.md"
701
+ )
702
+ $s9N = 0
703
+ foreach ($row in $S9Rows) {
704
+ $p = $row -split '\|'
705
+ $lid = $p[0]; $lFile = $p[1]; $consumer = $p[2]
706
+ if ([string]::IsNullOrEmpty($lid)) { continue }
707
+ $s9N++
708
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $lFile) -PathType Leaf)) {
709
+ Write-Skip "SHAPE-9 $lid`: not executable — $lFile is absent (the layer may already be gone)"
710
+ continue
711
+ }
712
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $consumer) -PathType Leaf)) {
713
+ Write-Skip "SHAPE-9 $lid`: not executable — the consumer named for the gap is absent"
714
+ continue
715
+ }
716
+ $stem = [System.IO.Path]::GetFileNameWithoutExtension($lFile)
717
+ if ((Get-FullText (Join-Path $Base $consumer)).Contains($stem)) {
718
+ Write-Ok "SHAPE-9 $lid`: the gap is real — $consumer still calls it"
719
+ } else {
720
+ Write-Hit "SHAPE-9 $lid`: $consumer no longer calls it — the gap it exists for is gone"
721
+ }
722
+ }
723
+ if ($s9N -eq 0) { Write-Skip "SHAPE-9: no compatibility layer is registered" }
724
+
725
+ # =======================================================================================
726
+ # SHAPE 10 — a general rule that exists only as an instance
727
+ # =======================================================================================
728
+ Write-Output " [10] a general rule that exists only as an instance"
729
+ $S10Rows = @(
730
+ "detect-then-ask|detect-then-ask|commands/specpro.implement.md commands/specpro.plan.md commands/specpro.tasks.md commands/specpro.test-plan.md commands/specpro.specify.md"
731
+ )
732
+ $s10N = 0
733
+ foreach ($row in $S10Rows) {
734
+ $p = $row -split '\|'
735
+ $rid = $p[0]; $statement = $p[1]; $scope = $p[2]
736
+ if ([string]::IsNullOrEmpty($rid)) { continue }
737
+ $s10N++
738
+ $missing = @()
739
+ $seen = 0
740
+ foreach ($f in ($scope -split ' ')) {
741
+ if ([string]::IsNullOrEmpty($f)) { continue }
742
+ $fPath = Join-Path $Base $f
743
+ if (-not (Test-Path -LiteralPath $fPath -PathType Leaf)) { continue }
744
+ $seen++
745
+ if (-not (Test-HasMatch -Path $fPath -Pattern $statement)) { $missing += $f }
746
+ }
747
+ if ($seen -eq 0) {
748
+ Write-Skip "SHAPE-10 $rid`: not executable — none of the declared scope exists"
749
+ } elseif (@($missing).Count -gt 0) {
750
+ $miss = " " + ($missing -join " ")
751
+ Write-Hit "SHAPE-10 $rid`: stated in $($seen - @($missing).Count) of $seen declared files; missing:$miss"
752
+ } else {
753
+ Write-Ok "SHAPE-10 $rid`: stated in all $seen files of its declared scope"
754
+ }
755
+ }
756
+ if ($s10N -eq 0) { Write-Skip "SHAPE-10: no rule is registered" }
757
+
758
+ # =======================================================================================
759
+ # SHAPE 11 — the criterion's conclusion is not a function of its input
760
+ # =======================================================================================
761
+ Write-Output " [11] the criterion's conclusion is not a function of its input"
762
+ $S11Rows = @("verify-ledger|scripts/bash/verify-ledger.sh")
763
+ $s11N = 0
764
+ $s11Runs = 3
765
+ $bashCmd = Get-Command bash -ErrorAction SilentlyContinue
766
+ foreach ($row in $S11Rows) {
767
+ $p = $row -split '\|'
768
+ $cid = $p[0]; $runner = $p[1]
769
+ if ([string]::IsNullOrEmpty($cid)) { continue }
770
+ $s11N++
771
+ if (-not (Test-Path -LiteralPath (Join-Path $Base $runner) -PathType Leaf)) {
772
+ Write-Skip "SHAPE-11 $cid`: not executable — runner $runner is absent"
773
+ continue
774
+ }
775
+ if (-not $bashCmd) {
776
+ Write-Skip "SHAPE-11 $cid`: not executable — no bash on this host to run the runner"
777
+ continue
778
+ }
779
+ $first = $null
780
+ $varies = $false
781
+ for ($i = 1; $i -le $s11Runs; $i++) {
782
+ Push-Location $Base
783
+ $out = @(& bash $runner 2>&1) -join "`n"
784
+ Pop-Location
785
+ if ($null -eq $first) { $first = $out } elseif ($out -ne $first) { $varies = $true }
786
+ }
787
+ if ($varies) {
788
+ Write-Hit "SHAPE-11 $cid`: $s11Runs runs on one input disagreed — the verdict is not a function of the input"
789
+ } else {
790
+ Write-Ok "SHAPE-11 $cid`: $s11Runs runs on one input agreed"
791
+ }
792
+ }
793
+ if ($s11N -eq 0) { Write-Skip "SHAPE-11: no criterion runner is registered" }
794
+
795
+ # =======================================================================================
796
+ # COVERAGE — the register and the detections must be the same set
797
+ # =======================================================================================
798
+ # 判据 (形态 4 applied to this file itself): the report states how many shapes were
799
+ # checked, and that number MUST be re-derived — from the detections this file defines,
800
+ # and from the REGISTER the claim is about.
801
+ #
802
+ # ⚠️ Why this block exists (`ISS-171` / `T211`): the verdict used to read
803
+ # `shapes: 11 checked` — a LITERAL. Registering a 12th shape (in both carriers, so
804
+ # pre-commit's own sync invariant still holds) produced `11 checked` and `rc=0`: a shape
805
+ # with no detection at all, reported as completely covered. That is 形态 6's shape — the
806
+ # step was there, but its verdict could not be anything but pass — and it is why `T132`,
807
+ # whose whole job is "every registered shape has a detection", could not fail.
808
+ #
809
+ # ⚠️ One derived number, against the register — and it MUST be the register, not this
810
+ # file's own count. Comparing the report only to its own detections would catch an edited
811
+ # comment and nothing else; it is the REGISTER that the report makes a claim about.
812
+ #
813
+ # ⚠️ The marker is `# SHAPE n`, and it is the SAME regex on both implementations on purpose.
814
+ # An earlier version counted the section-heading idiom instead, which is `echo " [n] …" on
815
+ # the Bash side and `Write-Output " [n] …"` here — so the two sides counted different
816
+ # things and this half read 0. The parity suite caught it, and the fix is to count the one
817
+ # marker both files actually share.
818
+ Write-Output " [coverage] the register and the detections must be the same set"
819
+
820
+ $Self = Join-Path $PSScriptRoot "verify-shapes.ps1"
821
+ $shapesChecked = Get-Count $Self '^# SHAPE [0-9]+'
822
+ $register = Join-Path $Base "specs/plan.md"
823
+ if (-not (Test-Path -LiteralPath $register -PathType Leaf)) {
824
+ Write-Skip "COVERAGE: not executable — the shape register $register is absent, so 'every registered shape was checked' cannot be judged"
825
+ } else {
826
+ $shapesRegistered = Get-Count $register '^\| \*\*[0-9]+\*\* \|'
827
+ if ($shapesChecked -eq $shapesRegistered) {
828
+ Write-Ok "COVERAGE: $shapesChecked detection(s) for $shapesRegistered registered shape(s)"
829
+ } else {
830
+ Write-Hit "COVERAGE: the register declares $shapesRegistered shape(s) but this file defines $shapesChecked detection(s) — the difference has no detection"
831
+ }
832
+ }
833
+
834
+ # =======================================================================================
835
+ # SHAPE 12 — a silence with no receiver
836
+ # =======================================================================================
837
+ # 判据 (`spec.md` → FR-047 登记表第 12 行 · `plan.md` → Constraint 7 第 12 行,照引不重推):
838
+ # 一条**已登记**的检出,其作用域对某一**整类**对象为空(或全部为"不可行使");该 `⊘` 在
839
+ # **任务与台账里都零承接**即命中。
840
+ #
841
+ # ⚠️ **承接的匹配是「条目形状」的,不是「关键词出现」的** (`TOOL-009`): the ledger's own
842
+ # verifier already made this correction (`^- \[[x ]\] ISS-<N>:` rather than a full-text ID
843
+ # search). ⚠️ And the weaker check would pass on THIS FILE'S OWN COMMENTARY — the comments
844
+ # above name `T124` and `ISS-178` — which is the cleanest available demonstration that a
845
+ # full-text match answers a different question.
846
+ #
847
+ # ⚠️ **What a pass does and does not prove**, stated because reading it as the stronger
848
+ # claim would stop the search: it proves **the silences declared this run have receivers
849
+ # that exist as entries**. It does NOT prove that every silence declared a receiver — that
850
+ # judgement is made at the call site (`Write-Skip` vs `Write-Unreceived`) and is visible in
851
+ # the code, not decidable here.
852
+ Write-Output " [12] a silence with no receiver"
853
+
854
+ $s12N = 0
855
+ foreach ($pair in $script:Unreceived) {
856
+ $rcv = $pair[0]; $msg = $pair[1]
857
+ if ([string]::IsNullOrEmpty($msg)) { continue }
858
+ $s12N++
859
+ if ([string]::IsNullOrEmpty($rcv)) {
860
+ Write-Hit "SHAPE-12: a silence names no receiver — $msg"
861
+ continue
862
+ }
863
+ if (Test-ReceiversExist $rcv) {
864
+ Write-Ok "SHAPE-12 $rcv`: receives '$msg'"
865
+ } else {
866
+ Write-Hit "SHAPE-12 $rcv`: names a receiver that is not an entry in tasks.md or the ledger — $msg"
867
+ }
868
+ }
869
+ if ($s12N -eq 0) { Write-Skip "SHAPE-12: no silence was declared this run — nothing to receive, so nothing is proven" }
870
+
871
+
872
+ # =======================================================================================
873
+ # Constraint 7's detection column — a TWO-WAY map (`T222`)
874
+ # =======================================================================================
875
+ # 判据 (`plan.md` → Constraint 7 · `spec.md` → FR-047 形态 2 的判据, 照引不重推):
876
+ # 那张表的**每一行**点名一处检出,而**每一处检出都要有一行点名它**。
877
+ #
878
+ # ⚠️ **本条补的是哪个缺口** (`ISS-192`): `S2_CLAIMS` · `S2_CONSUMER_ROWS` · `COVERAGE`
879
+ # 三处**都在本文件内部自洽,没有一处读 `plan.md`**。而 `spec.md` 的判据**明确把检出方式
880
+ # 的真源指给 `plan.md`** ⇒ 那张表**指不到任何东西时不报错**,且它自称是真源。
881
+ $c7Rows = @(
882
+ "1|回归用例|SHAPE 1"
883
+ "2|S2_CLAIMS|S2_CLAIMS"
884
+ "3|全文反查|SHAPE 3"
885
+ "4|重跑|SHAPE 4"
886
+ "5|承接|SHAPE 5"
887
+ "6|使该判据为假的输入|SHAPE 6"
888
+ "7|生产者是不是被判定的实现本身|SHAPE 7"
889
+ "8|边界哨兵|SHAPE 8"
890
+ "9|缺口是否真实存在|SHAPE 9"
891
+ "10|同一表述|SHAPE 10"
892
+ "11|连跑 N 次|SHAPE 11"
893
+ "12|有谁承接它|SHAPE 12"
894
+ )
895
+ $c7Plan = Join-Path $Base "specs/plan.md"
896
+ $c7N = 0
897
+ if (-not (Test-Path -LiteralPath $c7Plan -PathType Leaf)) {
898
+ Write-Skip "SHAPE-2 c7-table: not executable — specs/plan.md is absent"
899
+ } else {
900
+ $c7SelfText = (Get-Lines $Self) -join "`n"
901
+ foreach ($row in $c7Rows) {
902
+ $c = $row -split '\|'
903
+ $num = $c[0]; $planTok = $c[1]; $scriptTok = $c[2]
904
+ if ([string]::IsNullOrEmpty($num)) { continue }
905
+ $c7N++
906
+ $planRow = @(Get-Lines $c7Plan | Where-Object { $_ -match ("^\| \*\*" + [regex]::Escape($num) + "\*\* \|") }) | Select-Object -First 1
907
+ if ([string]::IsNullOrEmpty($planRow)) {
908
+ Write-Hit "SHAPE-2 c7-$num`: Constraint 7 has no row for shape $num"
909
+ continue
910
+ }
911
+ if (-not $planRow.Contains($planTok)) {
912
+ Write-Hit "SHAPE-2 c7-$num`: the row's detection column does not name '$planTok'"
913
+ continue
914
+ }
915
+ if (-not $c7SelfText.Contains($scriptTok)) {
916
+ Write-Hit "SHAPE-2 c7-$num`: the row names a detection that does not exist here — '$scriptTok'"
917
+ continue
918
+ }
919
+ Write-Ok "SHAPE-2 c7-$num`: names '$planTok' → '$scriptTok' exists"
920
+ }
921
+ # (c) 反向: 三处计数必须相等
922
+ # ⚠️ 约束在 `#### Constraint 7` 那一节之内,理由同 Bash 侧
923
+ $c7PlanRows = 0; $inC7 = $false
924
+ foreach ($c7ln in (Get-Lines $c7Plan)) {
925
+ if ($c7ln -match '^#### Constraint 7') { $inC7 = $true; continue }
926
+ if ($inC7 -and ($c7ln -match '^#### ')) { $inC7 = $false; continue }
927
+ if ($inC7 -and ($c7ln -match '^\| \*\*[0-9]+\*\* \|')) { $c7PlanRows++ }
928
+ }
929
+ if ($c7PlanRows -ne $c7N) {
930
+ Write-Hit "SHAPE-2 c7-table: Constraint 7 declares $c7PlanRows row(s), this registry maps $c7N"
931
+ }
932
+ if ($shapesChecked -ne $c7N) {
933
+ Write-Hit "SHAPE-2 c7-table: this file defines $shapesChecked detection(s), this registry maps $c7N"
934
+ }
935
+ if ($c7N -eq 0) { Write-Skip "SHAPE-2 c7-table: the registry is empty — nothing is proven" }
936
+ }
937
+
938
+ # =======================================================================================
939
+ # `specs/audit/` — NO COMMAND MAY DECLARE IT (`T221`)
940
+ # =======================================================================================
941
+ # 判据 (`plan.md` → Pattern 7 的边界裁定 · `specs/contracts/dev-audit-tool.md`):
942
+ # `specs/audit/` 下的产物**不属于任何命令** —— 它们的生产者是**维护者的人工轮次**。
943
+ # ⇒ 任何命令的 `writes:` 里出现它即**越界**。
944
+ # ⚠️ **输出格式必须与 `grep -rn` 逐字节一致** —— `path:line:text`。FR-008 的 parity 契约比的是
945
+ # stdout 的每一个字节,所以这里不能只写行内容;`_Pair` 事后把两棵树各自的根替换成 <ROOT>。
946
+ $c7Audit = @()
947
+ foreach ($f in (Get-ChildItem -Path (Join-Path $Base "commands") -Filter *.md -File -ErrorAction SilentlyContinue | Sort-Object Name)) {
948
+ $ln_no = 0
949
+ foreach ($ln in (Get-Lines $f.FullName)) {
950
+ $ln_no++
951
+ if ($ln -match 'artifact: specs/audit') { $c7Audit += ("$($f.FullName):$ln_no`:$ln") }
952
+ }
953
+ }
954
+ if ($c7Audit.Count -gt 0) {
955
+ foreach ($ln in $c7Audit) {
956
+ if ([string]::IsNullOrEmpty($ln)) { continue }
957
+ Write-Hit "SHAPE-2 audit-boundary: a command declares an artifact no command owns — $ln"
958
+ }
959
+ } else {
960
+ Write-Ok "SHAPE-2 audit-boundary: no command declares specs/audit/"
961
+ }
962
+
963
+ # =======================================================================================
964
+ # Verdict
965
+ # =======================================================================================
966
+ Write-Output ""
967
+ Write-Output "shapes: $shapesChecked checked · $($script:Oks) clean · $($script:Hits) hit · $($script:Skips) not executable"
968
+ if ($script:Hits -eq 0) {
969
+ Write-Output "✓ shapes: no registered shape has a live instance"
970
+ Write-Output " ⚠️ coverage: this is 'no known instance' for the registered subjects only."
971
+ Write-Output " It is NOT a statement that the tool is free of defects. $($script:Skips) subject group(s)"
972
+ Write-Output " could not be exercised at all, and each such group is unproven, not clean."
973
+ exit 0
974
+ } else {
975
+ Write-Output "✗ shapes: $($script:Hits) hit(s) — each one is a live instance of its shape"
976
+ Write-Output " ⚠️ FR-048: 消除以重跑检出成立,不以「已修」的声明。"
977
+ exit 1
978
+ }