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.
- specpro_cli/__init__.py +16 -0
- specpro_cli/assets/commands/specpro.analyze.md +1102 -0
- specpro_cli/assets/commands/specpro.checklist.md +335 -0
- specpro_cli/assets/commands/specpro.clarify.md +581 -0
- specpro_cli/assets/commands/specpro.constitution.md +488 -0
- specpro_cli/assets/commands/specpro.feature.md +115 -0
- specpro_cli/assets/commands/specpro.implement.md +1881 -0
- specpro_cli/assets/commands/specpro.manual-test.md +206 -0
- specpro_cli/assets/commands/specpro.plan.md +3284 -0
- specpro_cli/assets/commands/specpro.qc.md +1489 -0
- specpro_cli/assets/commands/specpro.scenarios.md +154 -0
- specpro_cli/assets/commands/specpro.specify.md +1449 -0
- specpro_cli/assets/commands/specpro.status.md +863 -0
- specpro_cli/assets/commands/specpro.tasks.md +1207 -0
- specpro_cli/assets/commands/specpro.test-implement.md +462 -0
- specpro_cli/assets/commands/specpro.test-plan.md +383 -0
- specpro_cli/assets/commands/specpro.user-manual.md +178 -0
- specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
- specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
- specpro_cli/assets/scripts/bash/common.sh +88 -0
- specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
- specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
- specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
- specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
- specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
- specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
- specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
- specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
- specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
- specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
- specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
- specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
- specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
- specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
- specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
- specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
- specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
- specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
- specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
- specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
- specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
- specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
- specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
- specpro_cli/assets/templates/agent-context-template.md +49 -0
- specpro_cli/assets/templates/assumptions-template.md +248 -0
- specpro_cli/assets/templates/checklist-template.md +40 -0
- specpro_cli/assets/templates/clarifications-template.md +155 -0
- specpro_cli/assets/templates/constitution-template.md +50 -0
- specpro_cli/assets/templates/feature-spec-template.md +66 -0
- specpro_cli/assets/templates/plan-overview-template.md +150 -0
- specpro_cli/assets/templates/plan-template.md +387 -0
- specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
- specpro_cli/assets/templates/requirements-template.md +356 -0
- specpro_cli/assets/templates/spec-template.md +267 -0
- specpro_cli/assets/templates/tasks-template.md +252 -0
- specpro_cli/assets/templates/test-tasks-template.md +174 -0
- specpro_cli/cli/__init__.py +5 -0
- specpro_cli/cli/cmd_init.py +416 -0
- specpro_cli/cli/cmd_remove.py +122 -0
- specpro_cli/cli/entry.py +181 -0
- specpro_cli/integrations/__init__.py +36 -0
- specpro_cli/integrations/base.py +601 -0
- specpro_cli/integrations/claude/__init__.py +101 -0
- specpro_cli/integrations/copilot/__init__.py +153 -0
- specpro_cli/integrations/cursor_agent/__init__.py +51 -0
- specpro_cli/integrations/gemini/__init__.py +44 -0
- specpro_cli/integrations/opencode/__init__.py +48 -0
- specpro_cli/integrations/qodercli/__init__.py +54 -0
- specpro_cli/integrations/registry.py +88 -0
- specpro_cli/packaged/__init__.py +5 -0
- specpro_cli/packaged/sync.py +106 -0
- specpro_cli-0.1.0.dist-info/METADATA +117 -0
- specpro_cli-0.1.0.dist-info/RECORD +76 -0
- specpro_cli-0.1.0.dist-info/WHEEL +4 -0
- specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|