dsh-logicprobe 0.9.0 → 1.0.0
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.
- package/README.en-US.md +79 -14
- package/README.md +79 -14
- package/lib/baseline.js +142 -0
- package/lib/client.js +56 -1
- package/lib/compose-tool.js +1 -1
- package/lib/concurrency.js +14 -0
- package/lib/data-engine.js +42 -8
- package/lib/engine.js +379 -79
- package/lib/export-tool.js +27 -2
- package/lib/granularity.js +237 -0
- package/lib/index.js +63 -41
- package/lib/report-diff-tool.js +39 -0
- package/lib/structure-tool.js +64 -0
- package/lib/structure.js +648 -0
- package/lib/tool.js +3 -3
- package/lib/types/baseline.d.ts +69 -0
- package/lib/types/concurrency.d.ts +10 -0
- package/lib/types/data-engine.d.ts +13 -1
- package/lib/types/engine.d.ts +138 -1
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +9 -8
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +156 -0
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/uml.d.ts +89 -0
- package/lib/uml-tool.js +36 -7
- package/lib/uml.js +217 -23
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -14
- package/skills/logicprobe/SKILL.md +84 -58
- package/skills/logicprobe/references/dsh-model-schema.md +81 -10
- package/skills/logicprobe/references/gap-routing-guide.md +37 -36
- package/skills/logicprobe/references/hash-spec.md +128 -0
- package/skills/logicprobe/references/logic-verification-guide.md +27 -250
- package/skills/logicprobe-concurrency/SKILL.md +43 -0
- package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
- package/skills/logicprobe-datamodel/SKILL.md +5 -4
- package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
- package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
- package/skills/logicprobe-structure/SKILL.md +95 -0
- package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
- package/skills/logicprobe-uml/SKILL.md +55 -0
- package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
- package/src/baseline.ts +180 -0
- package/src/client.js +56 -1
- package/src/compose-tool.ts +47 -47
- package/src/concurrency.ts +23 -0
- package/src/data-engine.ts +51 -9
- package/src/engine.ts +441 -67
- package/src/export-tool.ts +73 -48
- package/src/granularity.ts +312 -0
- package/src/index.ts +63 -41
- package/src/report-diff-tool.ts +43 -0
- package/src/structure-tool.ts +68 -0
- package/src/structure.ts +724 -0
- package/src/tool.ts +3 -3
- package/src/uml-tool.ts +38 -8
- package/src/uml.ts +275 -24
- package/skills/logicprobe/references/logicprobe-engine.py +0 -4943
- package/skills/logicprobe/references/verification-harness.py +0 -959
- package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Structure Review Guide
|
|
2
|
+
|
|
3
|
+
How to audit a component, package, class or deployment diagram as a dependency graph: the matrix schema, every finding code, a worked example, and the method for reconciling the diagram with a source-side check.
|
|
4
|
+
|
|
5
|
+
The tool is `logicprobe_structure_verify`. A host without it runs the repository engine on the same diagram text; the parity test compares the two field for field.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
logicprobe_structure_verify { diagram: "<PlantUML or Mermaid class text>", notation?: auto|plantuml|mermaid, matrix?: {...} }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Accepted diagram families**: PlantUML `component`, `package`, `class`, `interface`, `enum`, `object`, `actor`, `usecase`, `database`, `node`, `artifact`, `deployment`, `rectangle`, `folder`, `frame`, `cloud`, `queue`, `stack`, `storage`, `collections`, `agent`, `boundary`, `control`, `entity`, `namespace`, `module`; Mermaid `classDiagram` (`class X`, `X --> Y : label`, `X ..|> Y`).
|
|
14
|
+
|
|
15
|
+
**Edge forms** (PlantUML): `-->`, `->`, `..>`, `--`, `==>`, `<--`, `<..`. Mermaid class: `-->`, `..>`, `--|>`, `..|>`, `--`, `..`.
|
|
16
|
+
|
|
17
|
+
**What is read**: declarations (with `as` aliases and quoted labels), container nesting (`package "app" { … }`), arrows with labels, and note blocks (skipped — a note is not a dependency). Anything else is reported as `STRUCTURE_IGNORED_LINE` in `warnings`, never silently dropped.
|
|
18
|
+
|
|
19
|
+
**Node identity**: the alias/id when one exists (`component [Order Service] as SVC` → `SVC`), otherwise the declared name. An endpoint that only ever appears in an arrow still becomes a node, marked `declared: false` — that is what `UML021` reports.
|
|
20
|
+
|
|
21
|
+
## Matrix schema
|
|
22
|
+
|
|
23
|
+
```jsonc
|
|
24
|
+
{
|
|
25
|
+
"rules": [
|
|
26
|
+
{
|
|
27
|
+
"id": "R1", // required, unique; this is what the report cites
|
|
28
|
+
"source": "app.*", // node id or glob; absent = the rule judges no source
|
|
29
|
+
"allow": ["hal.*"], // permitted targets (glob or id)
|
|
30
|
+
"deny": ["hal.secret"], // forbidden targets; deny wins over allow, globally
|
|
31
|
+
"require": false // true = an edge matching source→allow must exist (UML025)
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"layers": [
|
|
35
|
+
{ "name": "app", "members": ["SVC", "PAY"] }, // top first
|
|
36
|
+
{ "name": "hal", "members": ["DRV", "DB"] }
|
|
37
|
+
],
|
|
38
|
+
"default": "allow" // or "deny": anything unlisted is a violation
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rules:
|
|
43
|
+
|
|
44
|
+
- Glob: `*` matches any run of characters (including `.` and `:`), `?` exactly one. `app.*` matches `app.motion`, not `hal.app`.
|
|
45
|
+
- **Deny is global.** Evaluation collects every rule whose `source` matches the edge, then: any deny match ⇒ violation; else any allow match ⇒ permitted; else the matrix `default` decides (`default` basis vs `unlisted` basis). The verdict cannot depend on rule order.
|
|
46
|
+
- `default: "allow"` is the permissive default: rules are exceptions. `default: "deny"` is the closed-world form and is what most architecture tables actually mean.
|
|
47
|
+
- Layers are independent of rules and are evaluated on every edge whose endpoints both belong to a layer: index order is top→bottom, so `app → hal` is allowed and `hal → app` is `UML024`.
|
|
48
|
+
- An invalid matrix is a hard error (`MATRIX_INVALID`) and no rules apply — a typo must never silently weaken the check. Unknown keys, duplicate rule ids, non-string patterns and `require` without `allow` are all rejected.
|
|
49
|
+
|
|
50
|
+
## Finding codes
|
|
51
|
+
|
|
52
|
+
| Code | Severity | Evidence | How to read it |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `UML020_ISOLATED_NODE` | warning | `nodes[{id,line}]` | Declared, non-container, degree 0. Either a dead entry or a forgotten dependency. |
|
|
55
|
+
| `UML021_DANGLING_REFERENCE` | error | `nodes[{id,line}]` | An arrow endpoint no declaration introduced. The dependency targets something the diagram never defines. |
|
|
56
|
+
| `UML022_CYCLE` | error | `cycle[]`, `path[]` | Shortest directed cycle, start repeated (`A → B → C → A`). A self-loop is a cycle of two elements (`A → A`). |
|
|
57
|
+
| `UML023_DISALLOWED_EDGE` | error | `edge{from,to,line}`, `rule?`, `matchedRules[]`, `basis` | `basis: deny` means a rule forbade it; `basis: unlisted` means no rule allowed it under `default: deny`. |
|
|
58
|
+
| `UML024_LAYER_VIOLATION` | error | `violations[{from,to,fromLayer,toLayer,line}]` | An upward edge between declared layers. |
|
|
59
|
+
| `UML025_MISSING_EXPECTED_EDGE` | warning | `missing[{rule,source,expected,unsatisfiedFrom,noSourceMatch}]` | A `require: true` rule the diagram does not satisfy. `noSourceMatch` distinguishes "wrong target" from "no such source node". |
|
|
60
|
+
| `UML026_NO_NODES` | error | `notation` | Nothing to review; the text is not a structure diagram. |
|
|
61
|
+
| `MATRIX_INVALID` | error | — | The matrix was rejected; its messages name the exact field. |
|
|
62
|
+
|
|
63
|
+
Every report also carries `summary` (nodes, edges, containers, isolated, dangling, cycles, disallowedEdges, layerViolations, missingExpectedEdges, errors, warnings, rules), `edgeVerdicts` when a matrix was supplied, and `verdict` / `verdictReason`. A structure diagram with an error finding is `verdict: "fail"` even though `ok: true` — `ok` only means the tool ran.
|
|
64
|
+
|
|
65
|
+
## Worked example
|
|
66
|
+
|
|
67
|
+
The diagram (a real shape: two packages, an orphan, a back edge, an undeclared endpoint):
|
|
68
|
+
|
|
69
|
+
```plantuml
|
|
70
|
+
@startuml
|
|
71
|
+
package "app" {
|
|
72
|
+
component [Order Service] as SVC
|
|
73
|
+
component [Payment Adapter] as PAY
|
|
74
|
+
}
|
|
75
|
+
package "hal" {
|
|
76
|
+
component [Storage Driver] as DRV
|
|
77
|
+
}
|
|
78
|
+
component [Orphan Cache] as CACHE
|
|
79
|
+
SVC --> PAY : plan
|
|
80
|
+
PAY --> DRV : read
|
|
81
|
+
DRV --> SVC : fault
|
|
82
|
+
SVC --> GHOST : unknown
|
|
83
|
+
@enduml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Without a matrix the structural checks already speak:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
UML021_DANGLING_REFERENCE (error) GHOST is never declared
|
|
90
|
+
UML020_ISOLATED_NODE (warning) CACHE has no edge
|
|
91
|
+
UML022_CYCLE (error) SVC → PAY → DRV → SVC
|
|
92
|
+
verdict: fail
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With the matrix from the skill body (`default: "deny"`, layers `app` over `hal`):
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
UML023_DISALLOWED_EDGE (error) DRV → SVC basis=deny (R3-no-back-edges)
|
|
99
|
+
UML023_DISALLOWED_EDGE (error) SVC → GHOST basis=unlisted (no rule allows it)
|
|
100
|
+
UML024_LAYER_VIOLATION (error) DRV (hal) → SVC (app)
|
|
101
|
+
verdict: fail, disallowedEdges: 2, layerViolations: 1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Both `UML022` and `UML023` fire on the same back edge: the cycle is a graph property, the violation is a matrix property. Report both — they are different findings with different fixes (a cycle needs restructuring; a disallowed edge needs either the edge removed or the matrix amended deliberately).
|
|
105
|
+
|
|
106
|
+
## Reconciliation with a source-side check
|
|
107
|
+
|
|
108
|
+
The diagram and an include/dependency scan over the source are two representations of one fact, and they drift. Keep the rule ids identical on both sides, then classify each difference:
|
|
109
|
+
|
|
110
|
+
| Difference | Reading | Action |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Code has the include, diagram has no edge | The diagram is stale | Redraw; `require: true` makes this a `UML025` warning every run |
|
|
113
|
+
| Diagram has the edge, code has no include | Either the diagram is aspirational, or the scan scope is narrower (generated files, `#ifdef`, build-time includes) | State which, with both citations |
|
|
114
|
+
| Code violates a rule the diagram never showed | The architecture defect the diagram hid | Fix the code, or amend the rule deliberately and record why |
|
|
115
|
+
| Both agree an edge is forbidden | A real violation, agreed by two methods | Act on it |
|
|
116
|
+
|
|
117
|
+
Never present the diagram's clean verdict as evidence about the code: it is evidence about the drawing. The reconciliation is the part that touches the source.
|
|
118
|
+
|
|
119
|
+
## Multi-granularity: one architecture at several levels
|
|
120
|
+
|
|
121
|
+
A module-level diagram and the repository-level diagram it belongs to are two views of one
|
|
122
|
+
fact, and nothing keeps them in step. Declare the relation and both are checked:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
logicprobe_structure_verify { diagrams: [ { name, diagram, parent? }, … ] }
|
|
126
|
+
logicprobe_structure_verify { diagrams: [ { name: "L1", diagram: "…", parent: "L0" }, … ] }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Per-diagram structural checks run first (UML020-UML026), and every finding is tagged with
|
|
130
|
+
the `file` it came from, so a report over several levels never carries an anonymous
|
|
131
|
+
finding. Then each declared pair is checked:
|
|
132
|
+
|
|
133
|
+
| Code | Severity | What it means |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `UML028_UNKNOWN_PARENT` | error | The `parent` names a diagram that is not in the set. |
|
|
136
|
+
| `UML029_REFINEMENT_VIOLATION` | error | **invented-edge**: the child draws a dependency between two nodes the parent also has, but the parent does not. **unexpanded-edge**: the parent draws an edge between two nodes the child also has, the child does not draw it, and no child path connects the same ends. |
|
|
137
|
+
| `UML030_PARENT_CYCLE` | error | The parent relation contains a cycle, so "which level owns this dependency" has no answer. |
|
|
138
|
+
| `UML031_NO_DIAGRAMS` | error | Nothing was supplied to compare. |
|
|
139
|
+
|
|
140
|
+
The rule is *refinement*, not equality: a child may add nodes and edges **below** the
|
|
141
|
+
parent level (an edge with an endpoint the parent does not have is the child's own
|
|
142
|
+
detail), and it may cover a **subtree** of the parent — a parent edge whose ends are not
|
|
143
|
+
both present in the child is out of scope for that level, not dropped. What it may never
|
|
144
|
+
do is invent a dependency at the parent level, or silently drop one it should have
|
|
145
|
+
expanded.
|
|
146
|
+
|
|
147
|
+
Each pair is summarised so the counts can be read without diffing two diagrams by eye:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{ "parent": "L0", "child": "L1",
|
|
151
|
+
"parentNodes": 3, "childNodes": 4, "parentEdges": 2, "childEdges": 4,
|
|
152
|
+
"inheritedEdges": 1, "newEdges": 2,
|
|
153
|
+
"expandedEdges": [{ "from": "SVC", "to": "PAY", "line": 5 }],
|
|
154
|
+
"missingEdges": [], "inventedEdges": [] }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A parent edge the child replaces with a path is a **legitimate expansion**, and it is
|
|
158
|
+
listed in `expandedEdges` rather than silently accepted: a reviewer confirms each one,
|
|
159
|
+
because that is where a refactoring quietly changes who calls whom. `hashes.diagrams`
|
|
160
|
+
records a sha256 per level, so a baseline diff can tell which level moved.
|
|
161
|
+
|
|
162
|
+
The same discipline as the single-diagram review applies: this checks the *diagrams*
|
|
163
|
+
against each other, not against the code. A level that is consistent with its parent but
|
|
164
|
+
not with the code is a drawing bug; a level that disagrees with its parent is a hierarchy
|
|
165
|
+
bug. Both need the source-side scan to settle.
|
|
166
|
+
|
|
167
|
+
## Extracting the matrix from an existing rule table
|
|
168
|
+
|
|
169
|
+
A machine-checked rule table (an include/dependency checker, a lint config, a review checklist) can seed the matrix: one rule entry per row, `id` = the row's own number so citations line up, `source`/`allow`/`deny` = the row's patterns, `layers` = the layering the table describes. Review the result before trusting it — a translation that silently widens `deny` into `allow` is worse than no check. Keep both files under version control together, and treat a matrix change as a code change.
|
|
170
|
+
|
|
171
|
+
## Limits
|
|
172
|
+
|
|
173
|
+
- The graph is what the diagram draws. Function pointers, DI, registries, plugins and dynamic dispatch are invisible.
|
|
174
|
+
- No transitive closure unless you write the rule for it.
|
|
175
|
+
- Containers (`package`, `rectangle`, `frame`) are exempt from the isolated-node check by design.
|
|
176
|
+
- `STRUCTURE_IGNORED_LINE` warnings mean the parser met a statement it does not model; treat a non-empty list as a signal that the diagram family may not be the one you think it is.
|
|
177
|
+
- Behaviour is out of scope: state machines, protocols, timing, budget, probability and deadlines belong to `logicprobe` (verification) and `logicprobe-uml` (the diagram round trip).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logicprobe-uml
|
|
3
|
+
description: "Use when drawing a code flow as UML or auditing a diagram someone drew: parse a hand-drawn state or activity diagram, render a model, and check round-trip fidelity (dead ends, unreachable states, label drift). Component, class and ER diagrams are refused, not modelled. Doctrine and behavioural verification: the logicprobe skill."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Logic Probe — UML Modelling
|
|
7
|
+
|
|
8
|
+
A diagram is a model, and a model can be wrong. A tidy flow chart is not evidence about the code, and a diagram that disagrees with its own model is worse than no diagram, because a reader will believe it.
|
|
9
|
+
|
|
10
|
+
This skill covers the diagram half of the Logic Probe toolbox. The claim-verification doctrine it inherits — enumerate verifiable claims, cite `file:line`, read `verdict` and never `ok` — lives in the `logicprobe` skill; load that one when the task is to verify behaviour rather than to draw or audit a diagram.
|
|
11
|
+
|
|
12
|
+
## When This Skill Applies
|
|
13
|
+
|
|
14
|
+
| The task involves… | What it does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Modelling a code flow as a state, activity or sequence diagram | `logicprobe_uml action=render`: model to diagram source, with a citation per state, event, guard and action |
|
|
17
|
+
| Reading a hand-drawn diagram back into a model | `action=parse`: Mermaid or PlantUML state/activity text into a LogicModelV1 that `logicprobe_verify` can check |
|
|
18
|
+
| Auditing a diagram somebody drew | `action=review`: structural defects, documentation gaps, and render-versus-model round-trip fidelity |
|
|
19
|
+
| A diagram whose claims must be tied to the code | Every element needs a source citation; the review says what the diagram says, not whether the code is correct |
|
|
20
|
+
|
|
21
|
+
Pipeline (DSH):
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
code flow → model (citation per element) → logicprobe_uml action=render → diagram source
|
|
25
|
+
→ logicprobe_uml action=review → modelling findings + round-trip fidelity
|
|
26
|
+
→ logicprobe_verify → behaviour (S1-S8 structural, A1-A14 adversarial)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **render** — model to diagram. Mermaid covers `state`, `activity` and `sequence`. PlantUML covers `state` and `sequence`. Any construct the notation cannot carry becomes a warning, never a silent drop. PlantUML activity is refused instead of approximated.
|
|
30
|
+
- **parse** — diagram to model. It reads Mermaid and PlantUML state or activity text, so a hand-drawn diagram can be verified like any other model. Two inputs are refused because they cannot become a machine: a sequence diagram (a trace cannot reconstruct a machine) and a different Mermaid family (`classDiagram`, `erDiagram`, `gantt`, `mindmap`, …) — both come back as `errorCode: "UML_NOT_A_STATE_DIAGRAM"` with the discarded construct named.
|
|
31
|
+
- **review** — it answers one of three questions. Give it a model: is the machine well-modelled? Give it a diagram: what does the diagram say? Give it both: does the diagram match the model?
|
|
32
|
+
|
|
33
|
+
A host without the tools runs the same three actions through the repository engine. The engine is deliberately single-source — do not copy it into this skill.
|
|
34
|
+
|
|
35
|
+
## A Structure Diagram Is the Dangerous Case, Because It Parses
|
|
36
|
+
|
|
37
|
+
PlantUML component, package, class and deployment diagrams declare no diagram kind, so the parser meets their keywords line by line. It still returns *something* — the arrows look like transitions — so `parse` reports the by-product model **plus** an error finding `UML_NOT_A_STATE_DIAGRAM`, the declarations it could not represent in `discardedConstructs` (with line and text), the arrows that were misread in `discardedEdges`, and `verdict: "fail"`.
|
|
38
|
+
|
|
39
|
+
Read that as: **there is no model of this file; the diagram was not checked.** Do not feed that model to `logicprobe_verify` and call the result a review of the architecture, and do not present a component diagram as evidence about dependencies. A structure diagram has its own reviewer: the `logicprobe-structure` skill and the `logicprobe_structure_verify` tool parse the same text into a real dependency graph and check isolated nodes, dangling endpoints, cycles, allowed-dependency and layer violations (`UML020`-`UML026`). Use that one when the question is *where the dependencies point*.
|
|
40
|
+
|
|
41
|
+
Mermaid families with an explicit header (`classDiagram`, `erDiagram`, `gantt`, …) are refused outright, because an empty model would be worse than no model.
|
|
42
|
+
|
|
43
|
+
## What the Review Reports
|
|
44
|
+
|
|
45
|
+
Structural defects: unreachable states, dead ends, ambiguous or non-exhaustive branches, self-loops with no exit, duplicate transitions. Documentation gaps: a missing narrative, unbounded variables, states the reader cannot map back to code, and label drift between diagram and narrative. It also runs the fidelity check — any structural difference between the diagram and its model is `UML017_ROUND_TRIP_MISMATCH`.
|
|
46
|
+
|
|
47
|
+
## Rules for This Mode
|
|
48
|
+
|
|
49
|
+
1. **A diagram is not evidence.** Every state, event, guard and action needs a citation. Use `file:line` for code and a section reference for a document. Present the citations with the diagram.
|
|
50
|
+
2. **Review before you present.** Run `logicprobe_uml action=review` and fix the error findings first. An ambiguous or dead-ended diagram misleads every later reader.
|
|
51
|
+
3. **Check the verdict before the picture.** `ok: true` with `verdict: "fail"` means the review failed; a diagram whose review failed is not something to show as if it were checked.
|
|
52
|
+
4. **The review never replaces verification.** It covers the modelling. S1-S8 and A1-A14 cover the behaviour, and each finding names the check that settles it — hand those over to the `logicprobe` skill.
|
|
53
|
+
5. **Keep the narrative with the model.** Write `narrative.states`, `narrative.events` and `narrative.scenarios`. Then the diagram stays readable against the code, and label drift shows up as a finding instead of as a stale picture.
|
|
54
|
+
|
|
55
|
+
The full checklist (codes `UML001` to `UML019` plus `UML_NOT_A_STATE_DIAGRAM`), the directive format generated diagrams carry, a worked example, and the limits of each view are in [`references/uml-modeling-guide.md`](references/uml-modeling-guide.md).
|
|
@@ -28,7 +28,7 @@ One machine, three projections. They are not interchangeable. The review reports
|
|
|
28
28
|
|
|
29
29
|
## Modelling a code flow from source
|
|
30
30
|
|
|
31
|
-
Extraction follows the same discipline as `logic-verification-guide.md`, applied to code instead of a plan.
|
|
31
|
+
Extraction follows the same discipline as `logic-verification-guide.md`, applied to code instead of a plan. That guide ships with the entry-point skill: `../logicprobe/references/logic-verification-guide.md`.
|
|
32
32
|
|
|
33
33
|
1. **Fix the boundary.** Decide which function, task, or module is the machine. In scope: state the code holds across calls, such as statics, fields, enums, and task state variables. Out of scope: the call stack inside one invocation, hardware behaviour, and scheduler preemption.
|
|
34
34
|
2. **Name the states from the code.** A state exists if something survives a call boundary and is tested later. An enum, a `state` field, or a task-local variable that gates the next entry all qualify. A local variable inside one function does not.
|
|
@@ -55,11 +55,7 @@ In DSH:
|
|
|
55
55
|
|
|
56
56
|
`logicprobe_uml` with `action=render` returns the diagram source plus `warnings`. Read those warnings. They list every construct the notation could not carry verbatim: a state id that needed an alias, a label containing `[` or `/`, a trace that was capped.
|
|
57
57
|
|
|
58
|
-
Without the DSH tool, run the
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
python skills/logicprobe/references/logicprobe-engine.py uml-render model.json --notation mermaid --diagram state
|
|
62
|
-
```
|
|
58
|
+
Without the DSH tool, run the repository engine on the same model JSON.
|
|
63
59
|
|
|
64
60
|
You can also write the diagram by hand. The generator is a convenience, not a requirement. A hand-written diagram is parsed and reviewed exactly like a generated one.
|
|
65
61
|
|
|
@@ -99,6 +95,7 @@ Four input shapes, four different questions:
|
|
|
99
95
|
| Code | Severity | What it means |
|
|
100
96
|
|---|---|---|
|
|
101
97
|
| `UML001_DIAGRAM_UNREADABLE` | error | The text is not readable as a Mermaid or PlantUML state or activity diagram. |
|
|
98
|
+
| `UML_NOT_A_STATE_DIAGRAM` | error | The text belongs to another diagram family. Numbers are reserved for `UML0xx` modelling findings, so this one carries a name instead; see [Structure diagrams are not models](#structure-diagrams-are-not-models). |
|
|
102
99
|
| `UML002_UNREACHABLE_STATE` | error | No transition can enter this state from init. The diagram draws flow nobody can reach. The check is structural and ignores guards; S1 is the guard-aware check. |
|
|
103
100
|
| `UML003_DEAD_END_STATE` | error | A non-terminal state has no outgoing transition. Either it is terminal, or the outgoing flow was never modelled. |
|
|
104
101
|
| `UML004_AMBIGUOUS_BRANCH` | error | Two unconditional arrows share one state and event. No reader and no implementation can resolve that. S4 is the authoritative check. |
|
|
@@ -110,7 +107,8 @@ Four input shapes, four different questions:
|
|
|
110
107
|
| `UML010_UNUSED_VARIABLE` | warning | No guard reads this variable and no action writes it. It is a symbol with no source. |
|
|
111
108
|
| `UML011_UNBOUNDED_VARIABLE` | info | An integer variable has no min or max, so no range invariant can be checked and A5 has no declared domain. |
|
|
112
109
|
| `UML012_NO_TERMINAL` | warning | No state is terminal, so completion, failure and a stuck flow all look the same. |
|
|
113
|
-
| `UML013_NO_NARRATIVE` | info | The model carries no meanings, so a reader must re-derive every symbol from the source. |
|
|
110
|
+
| `UML013_NO_NARRATIVE` | info | The model carries no meanings, so a reader must re-derive every symbol from the source. Adding a partial narrative is a valid first step. |
|
|
111
|
+
| `UML027_NARRATIVE_PARTIAL` | info | The narrative covers part of the model. `narrativeCoverage` (also in the finding's `evidence`) reports `covered/total` per dimension — states, events, scenarios. Incomplete is not wrong. |
|
|
114
112
|
| `UML014_UNDOCUMENTED_STATE` | info | These states render as their bare id, so the diagram cannot be read against the code. |
|
|
115
113
|
| `UML015_LABEL_DRIFT` | warning | A diagram label disagrees with the model narrative. One of the two is stale, and the review cannot tell which. |
|
|
116
114
|
| `UML016_DIAGRAM_PARSE_NOTES` | info | Notes collected while rendering or reading the diagram. They mark information the notation could not carry. |
|
|
@@ -118,7 +116,77 @@ Four input shapes, four different questions:
|
|
|
118
116
|
| `UML018_FIDELITY_UNCHECKED` | info | Only a diagram was supplied, so nothing here proves it matches the code. |
|
|
119
117
|
| `UML019_ROUND_TRIP_SKIPPED` | warning | The fidelity check could not run, either because the view is not round-trippable or because it was switched off. |
|
|
120
118
|
|
|
121
|
-
`ok: true` means the review ran. It does not mean the model is good. Read
|
|
119
|
+
`ok: true` means the review ran. It does not mean the model is good. Read `verdict`, not `ok`:
|
|
120
|
+
|
|
121
|
+
| `verdict` | What it means | What to do |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `pass` | No error and no warning finding. | Safe to present, with the scope the model covers. |
|
|
124
|
+
| `pass_with_findings` | No error finding, but warnings exist (e.g. `UML012_NO_TERMINAL`, `UML006`). | Present the pass **and** the warnings; do not upgrade it to "no issues". |
|
|
125
|
+
| `fail` | An error finding exists (`UML002`, `UML003`, `UML004`, `UML017`, `UML_NOT_A_STATE_DIAGRAM`, …) or the diagram could not be read (`ok: false`). | The diagram is **not** reviewed. Fix the error findings, or stop calling the file a model. |
|
|
126
|
+
|
|
127
|
+
`verdictReason` states why in one line (`2 error finding(s) (first: UML002_UNREACHABLE_STATE)`).
|
|
128
|
+
The non-DSH CLI (`uml-review`, `uml-parse`) exits `2` when the verdict is `fail` or the
|
|
129
|
+
input is refused, `0` otherwise.
|
|
130
|
+
|
|
131
|
+
### Structure diagrams are not models
|
|
132
|
+
|
|
133
|
+
This is the failure mode worth a section of its own, because it does not look like a failure.
|
|
134
|
+
|
|
135
|
+
A PlantUML component, package, class or deployment diagram declares no diagram kind —
|
|
136
|
+
it simply starts with `@startuml`. A state parser therefore meets its keywords line by
|
|
137
|
+
line, ignores the words it does not know, and reads the arrows as state transitions:
|
|
138
|
+
|
|
139
|
+
```plantuml
|
|
140
|
+
@startuml
|
|
141
|
+
component [Order Service] as SVC
|
|
142
|
+
component [Payment Adapter] as PAY
|
|
143
|
+
package "HAL" {
|
|
144
|
+
component [Storage Driver] as DRV
|
|
145
|
+
}
|
|
146
|
+
SVC --> PAY : request
|
|
147
|
+
PAY --> DRV : read
|
|
148
|
+
@enduml
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Before this contract existed, that parsed to `ok: true` with three "states" and two
|
|
152
|
+
"transitions", and "the structure diagram was reviewed" was pure fabrication. Now the
|
|
153
|
+
same input returns the by-product **plus**:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"verdict": "fail",
|
|
158
|
+
"findings": [{ "code": "UML_NOT_A_STATE_DIAGRAM", "severity": "error",
|
|
159
|
+
"message": "the text is not a state or activity diagram: 4 declaration(s) of unsupported construct(s) (component ×3, package ×1) and 2 arrow(s) between them were read as states and transitions, so the parsed model is not this diagram." }],
|
|
160
|
+
"discardedConstructs": [{ "construct": "component", "line": 2, "text": "component [Order Service] as SVC" }, "…"],
|
|
161
|
+
"discardedEdges": 2
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Read that as: **there is no model of this file.** Do not feed the by-product to
|
|
166
|
+
`logicprobe_verify` and call the result an architecture review, and do not quote its
|
|
167
|
+
"states" as dependencies. `discardedConstructs` lists every declaration that could not
|
|
168
|
+
be represented (with its line and text), and `discardedEdges` counts the arrows between
|
|
169
|
+
them that were misread.
|
|
170
|
+
|
|
171
|
+
Mermaid families with an explicit header — `classDiagram`, `erDiagram`, `gantt`,
|
|
172
|
+
`journey`, `mindmap`, `gitGraph`, `C4*`, `requirementDiagram`, `timeline`,
|
|
173
|
+
`quadrantChart`, `sankey-beta`, `block-beta`, `packet-beta`, `architecture-beta`,
|
|
174
|
+
`radar-beta`, `treemap-beta`, `xychart-beta` — are refused instead, because an empty
|
|
175
|
+
model would be worse than no model: the tool returns `ok: false` with
|
|
176
|
+
`errorCode: "UML_NOT_A_STATE_DIAGRAM"` naming the family, and `review` reports it as
|
|
177
|
+
`UML001_DIAGRAM_UNREADABLE`. Sequence diagrams are in the same class of refusal, for a
|
|
178
|
+
different reason (a trace cannot reconstruct a machine).
|
|
179
|
+
|
|
180
|
+
Both routes are non-pass and both name what was discarded. What the tool will **not**
|
|
181
|
+
do is hand back a plausible model with no verdict — that is the false guarantee.
|
|
182
|
+
|
|
183
|
+
**A structure diagram has its own reviewer.** Dependency-graph checks — isolated
|
|
184
|
+
nodes (`UML020`), dangling endpoints (`UML021`), cycles (`UML022`), disallowed edges
|
|
185
|
+
and layer violations against an allowed-dependency matrix (`UML023`, `UML024`) and
|
|
186
|
+
required-but-absent edges (`UML025`) — are implemented by the `logicprobe-structure`
|
|
187
|
+
skill and the `logicprobe_structure_verify` tool, which parse the same component,
|
|
188
|
+
package, class and deployment text into a real node/edge graph instead of discarding
|
|
189
|
+
it. Use that for architecture questions; this guide is about the state-machine half.
|
|
122
190
|
|
|
123
191
|
### What the round trip proves
|
|
124
192
|
|
|
@@ -156,6 +224,50 @@ Neither is a behaviour bug. Add the narrative so a reader can check the diagram
|
|
|
156
224
|
|
|
157
225
|
Now delete the `ERROR --cooldown--> STARTING` arrow and review again. `ACTIVE` and `ERROR` become dead ends (`UML003`), and `cooldown` becomes an event that only fires from an unreachable state (`UML007`). The diagram still looks plausible. That is the whole point.
|
|
158
226
|
|
|
227
|
+
## Labels and comments: what the parser expects
|
|
228
|
+
|
|
229
|
+
Two things cause most of the friction with hand-written diagrams. Both are printed by the
|
|
230
|
+
tool itself: `logicprobe_uml` does not have a flag for it (it documents the convention in
|
|
231
|
+
its description), and the CLI has `uml-review --explain-labels` (add `--notation
|
|
232
|
+
plantuml` for that flavour), which prints exactly the table below from the same rules the
|
|
233
|
+
parser applies.
|
|
234
|
+
|
|
235
|
+
**1. State meanings, in the spellings the parser accepts.** A label carries a *meaning*
|
|
236
|
+
when it differs from the bare id. Accepted, in this order:
|
|
237
|
+
|
|
238
|
+
| Spelling | Example | Notes |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| bare id | `state "IDLE" as IDLE` | counts as **no** meaning: the state is reported undocumented (`UML014`) |
|
|
241
|
+
| `ID(meaning)` | `state "IDLE(waiting for power)" as IDLE` | what the renderer writes; full-width parentheses |
|
|
242
|
+
| `ID(meaning)` | `state "IDLE(waiting for power)" as IDLE` | accepted as well |
|
|
243
|
+
| a description line | `IDLE : waiting for power` | the same meaning in the other PlantUML spelling |
|
|
244
|
+
| a single-line note | `note right of IDLE : waiting for power` | read as the meaning, not as a comment |
|
|
245
|
+
| any other text | `state "waiting for power" as IDLE` | taken verbatim |
|
|
246
|
+
|
|
247
|
+
`UML015_LABEL_DRIFT` compares the *meaning* with `narrative.states[id]` character for
|
|
248
|
+
character: the wrapper (`ID(…)`) is stripped, the rest must match. A different wording is
|
|
249
|
+
still drift — the review cannot tell which side is stale. Writing no label is not drift;
|
|
250
|
+
it is `UML014_UNDOCUMENTED_STATE` (info).
|
|
251
|
+
|
|
252
|
+
**2. Comment lines.** The only comment lines the parser *consumes* are the `logicprobe:`
|
|
253
|
+
directives the renderer writes (`%%logicprobe:` in Mermaid, `'logicprobe:` in PlantUML):
|
|
254
|
+
`uml v1 …`, `init ID`, `terminal ID`, `alias X id`, `variable NAME kind`. They are
|
|
255
|
+
ordinary comments to every renderer, which is why the diagram stays valid.
|
|
256
|
+
|
|
257
|
+
Everything else is either notation chrome (skipped silently) or an ignored line:
|
|
258
|
+
|
|
259
|
+
| Line | Behaviour |
|
|
260
|
+
|---|---|
|
|
261
|
+
| a `%%` / `'` comment that is not a directive | `UML_PARSE_IGNORED_LINE` warning — the line carried no statement |
|
|
262
|
+
| a multi-line `note … end note` block | skipped silently (prose is not a state meaning) |
|
|
263
|
+
| `@startuml`/`@enduml`, `stateDiagram-v2`, `direction`, `classDef`/`style`/`linkStyle`/`click`, `scale`, `skinparam`, `title`, `hide`, `autonumber` | skipped silently |
|
|
264
|
+
| anything else the parser cannot read | `UML_PARSE_IGNORED_LINE` warning, and the model is built from what it did read |
|
|
265
|
+
|
|
266
|
+
So the practical rule is: keep non-directive comments out of the diagram body (put render
|
|
267
|
+
commands and explanations around the diagram, not inside it), and give every state a
|
|
268
|
+
`ID(meaning)` label when the model carries a narrative — otherwise the diagram compares
|
|
269
|
+
as undocumented and a later drift check has nothing to compare against.
|
|
270
|
+
|
|
159
271
|
## Limits
|
|
160
272
|
|
|
161
273
|
- The review is **structural**. It evaluates no guard over any valuation. "Probably exhaustive" is a shape test, not a proof. S6 is the check that decides.
|
package/src/baseline.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report diffing: "did this slice add findings, or remove them?"
|
|
3
|
+
*
|
|
4
|
+
* The acceptance criterion of a refactoring slice is usually *not* "zero findings" —
|
|
5
|
+
* it is "no NEW findings, and none of the old ones got worse". Comparing two JSON
|
|
6
|
+
* reports by hand is how that gets fudged, so the comparison is code.
|
|
7
|
+
*
|
|
8
|
+
* A finding's identity is its machine-readable locator, never its prose:
|
|
9
|
+
* `check|code|<canonical evidence + path>`. Findings that carry neither evidence nor a
|
|
10
|
+
* path fall back to their message, because an engine that reports nothing structured
|
|
11
|
+
* has nothing stabler to match on. Two runs over the same input must produce zero
|
|
12
|
+
* added and zero removed findings.
|
|
13
|
+
*
|
|
14
|
+
* @module logicprobe-baseline
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { canonicalJson, REPORT_SCHEMAS, verdictOf, type Finding, type Verdict, type VerdictSummary } from './engine.js'
|
|
18
|
+
|
|
19
|
+
/** A finding as it appears in any report family, after flattening. */
|
|
20
|
+
export interface ReportFinding extends Finding {
|
|
21
|
+
/** The check that emitted it (verify/datamodel: `S2`, `C1`, …); absent for flat families. */
|
|
22
|
+
check?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BaselineChange {
|
|
26
|
+
identity: string
|
|
27
|
+
code: string
|
|
28
|
+
before: { severity: string; message: string }
|
|
29
|
+
after: { severity: string; message: string }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BaselineReport {
|
|
33
|
+
ok: boolean
|
|
34
|
+
ran: boolean
|
|
35
|
+
verdict: Verdict
|
|
36
|
+
verdictReason: string
|
|
37
|
+
/** The versioned report contract this result follows. */
|
|
38
|
+
schema: string
|
|
39
|
+
/** The current report's own verdict, echoed so "no new findings" cannot hide a failing run. */
|
|
40
|
+
currentVerdict: Verdict
|
|
41
|
+
added: ReportFinding[]
|
|
42
|
+
removed: ReportFinding[]
|
|
43
|
+
changed: BaselineChange[]
|
|
44
|
+
summary: {
|
|
45
|
+
baseline: number
|
|
46
|
+
current: number
|
|
47
|
+
added: number
|
|
48
|
+
removed: number
|
|
49
|
+
changed: number
|
|
50
|
+
addedErrors: number
|
|
51
|
+
addedWarnings: number
|
|
52
|
+
}
|
|
53
|
+
nextSteps: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The stable identity of a finding: check + file + code + a canonical locator. Prose is
|
|
58
|
+
* excluded so a reworded message reads as `changed`, not as a new finding.
|
|
59
|
+
*/
|
|
60
|
+
export function findingIdentity(finding: ReportFinding): string {
|
|
61
|
+
const locator = canonicalJson({
|
|
62
|
+
...(finding.evidence === undefined ? {} : { evidence: finding.evidence }),
|
|
63
|
+
...(finding.path === undefined ? {} : { path: finding.path }),
|
|
64
|
+
})
|
|
65
|
+
const where = locator === '{}' ? 'message:' + finding.message : locator
|
|
66
|
+
const file = finding.file === undefined ? '' : finding.file + '|'
|
|
67
|
+
return (finding.check === undefined ? '' : finding.check + '|') + file + finding.code + '|' + where
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Flatten a report of any family into one finding list (`checks[].findings` or `findings[]`). */
|
|
71
|
+
export function flattenReportFindings(report: unknown): ReportFinding[] {
|
|
72
|
+
if (report === null || typeof report !== 'object') return []
|
|
73
|
+
const record = report as Record<string, unknown>
|
|
74
|
+
const flat: ReportFinding[] = []
|
|
75
|
+
if (Array.isArray(record.checks)) {
|
|
76
|
+
for (const check of record.checks as Array<Record<string, unknown>>) {
|
|
77
|
+
const id = typeof check.id === 'string' ? check.id : undefined
|
|
78
|
+
for (const finding of (Array.isArray(check.findings) ? check.findings : []) as Finding[]) {
|
|
79
|
+
flat.push(id === undefined ? finding : { ...finding, check: id })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(record.findings)) {
|
|
84
|
+
for (const finding of record.findings as Finding[]) flat.push(finding)
|
|
85
|
+
}
|
|
86
|
+
return flat
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The report's own verdict, whatever family it came from. */
|
|
90
|
+
function verdictOfReport(report: unknown): Verdict {
|
|
91
|
+
if (report === null || typeof report !== 'object') return 'fail'
|
|
92
|
+
const value = (report as Record<string, unknown>).verdict
|
|
93
|
+
return value === 'pass' || value === 'pass_with_findings' || value === 'fail' ? value : 'fail'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function diffOf(added: number, removed: number, addedErrors: number, firstCode?: string): VerdictSummary {
|
|
97
|
+
if (addedErrors > 0) {
|
|
98
|
+
return {
|
|
99
|
+
verdict: 'fail',
|
|
100
|
+
verdictReason: String(added) + ' new finding(s) (' + String(addedErrors) + ' error)' + (firstCode === undefined ? '' : ' (first: ' + firstCode + ')'),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (added > 0) {
|
|
104
|
+
return { verdict: 'pass_with_findings', verdictReason: String(added) + ' new warning finding(s), no new errors' }
|
|
105
|
+
}
|
|
106
|
+
if (removed > 0) {
|
|
107
|
+
return { verdict: 'pass', verdictReason: 'no new findings; ' + String(removed) + ' finding(s) resolved' }
|
|
108
|
+
}
|
|
109
|
+
return { verdict: 'pass', verdictReason: 'no new findings, nothing resolved' }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compare a baseline report with the current one. The verdict describes the *delta*
|
|
114
|
+
* (a new error fails it), while `currentVerdict` keeps the absolute result visible.
|
|
115
|
+
*/
|
|
116
|
+
export function diffReports(baseline: unknown, current: unknown): BaselineReport {
|
|
117
|
+
const before = flattenReportFindings(baseline)
|
|
118
|
+
const after = flattenReportFindings(current)
|
|
119
|
+
const beforeByIdentity = new Map<string, ReportFinding>()
|
|
120
|
+
for (const finding of before) beforeByIdentity.set(findingIdentity(finding), finding)
|
|
121
|
+
const seen = new Set<string>()
|
|
122
|
+
const added: ReportFinding[] = []
|
|
123
|
+
const changed: BaselineChange[] = []
|
|
124
|
+
for (const finding of after) {
|
|
125
|
+
const identity = findingIdentity(finding)
|
|
126
|
+
seen.add(identity)
|
|
127
|
+
const previous = beforeByIdentity.get(identity)
|
|
128
|
+
if (previous === undefined) {
|
|
129
|
+
added.push(finding)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (previous.severity !== finding.severity || previous.message !== finding.message) {
|
|
133
|
+
changed.push({
|
|
134
|
+
identity,
|
|
135
|
+
code: finding.code,
|
|
136
|
+
before: { severity: previous.severity, message: previous.message },
|
|
137
|
+
after: { severity: finding.severity, message: finding.message },
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const removed = before.filter((finding) => !seen.has(findingIdentity(finding)))
|
|
142
|
+
const addedErrors = added.filter((finding) => finding.severity === 'error')
|
|
143
|
+
const addedWarnings = added.filter((finding) => finding.severity === 'warning')
|
|
144
|
+
const delta = diffOf(added.length, removed.length, addedErrors.length, addedErrors[0]?.code)
|
|
145
|
+
|
|
146
|
+
const nextSteps: string[] = []
|
|
147
|
+
if (addedErrors.length > 0) {
|
|
148
|
+
nextSteps.push('Fix or explicitly accept the ' + String(addedErrors.length) + ' new error finding(s) before calling this slice done: the baseline is what "not worse" is measured against.')
|
|
149
|
+
} else if (added.length > 0) {
|
|
150
|
+
nextSteps.push('The new warnings are not failures, but they are new: decide per finding whether the change introduced them or the baseline was incomplete.')
|
|
151
|
+
} else {
|
|
152
|
+
nextSteps.push('No new findings: re-record the baseline with this report if the slice is accepted, so the next comparison starts from it.')
|
|
153
|
+
}
|
|
154
|
+
if (changed.length > 0) nextSteps.push('Review the ' + String(changed.length) + ' changed finding(s): the same locator now reports something different, which usually means the fix moved the problem.')
|
|
155
|
+
if (removed.length > 0) nextSteps.push(String(removed.length) + ' finding(s) from the baseline are gone — confirm they were fixed rather than renamed out of the report (a renamed check or a reworded message changes the identity).')
|
|
156
|
+
if (verdictOfReport(current) === 'fail') nextSteps.push('The current report still fails on its own terms; the delta being clean does not make the run clean.')
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
ran: true,
|
|
161
|
+
...delta,
|
|
162
|
+
schema: REPORT_SCHEMAS.baseline,
|
|
163
|
+
currentVerdict: verdictOfReport(current),
|
|
164
|
+
added,
|
|
165
|
+
removed,
|
|
166
|
+
changed,
|
|
167
|
+
summary: {
|
|
168
|
+
baseline: before.length,
|
|
169
|
+
current: after.length,
|
|
170
|
+
added: added.length,
|
|
171
|
+
removed: removed.length,
|
|
172
|
+
changed: changed.length,
|
|
173
|
+
addedErrors: addedErrors.length,
|
|
174
|
+
addedWarnings: addedWarnings.length,
|
|
175
|
+
},
|
|
176
|
+
nextSteps,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type { VerdictSummary }
|
package/src/client.js
CHANGED
|
@@ -30,7 +30,61 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
id: 'dsh-logicprobe',
|
|
31
31
|
factory: (require) => {
|
|
32
32
|
const React = require('react')
|
|
33
|
-
|
|
33
|
+
// The host's Switch and Button, vendored. A plain-JS client bundle must not
|
|
34
|
+
// require a Harness Client package: it changes without notice, we get no type
|
|
35
|
+
// check, and a throw blanks this slot entry. Markup, classes, token usage and
|
|
36
|
+
// the aria behaviour follow the host primitive; classes carry our own prefix.
|
|
37
|
+
const CONTROLS_CSS = [
|
|
38
|
+
'.lp-switch { box-sizing: border-box; position: relative; flex: 0 0 auto; width: 36px; height: 20px; padding: 2px; border: 0; border-radius: 999px; corner-shape: round; background: var(--dsw-alias-border-l3); cursor: pointer; }',
|
|
39
|
+
".lp-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }",
|
|
40
|
+
'.lp-switch:disabled { cursor: default; opacity: 0.5; }',
|
|
41
|
+
'.lp-switch:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }',
|
|
42
|
+
'.lp-switch-thumb { display: block; width: 16px; height: 16px; border-radius: 50%; corner-shape: round; background: var(--dsw-alias-label-primary-foreground); transition: transform 120ms ease; }',
|
|
43
|
+
".lp-switch[aria-checked='false'] .lp-switch-thumb { background: var(--dsw-alias-switch-thumb); }",
|
|
44
|
+
".lp-switch[aria-checked='true'] .lp-switch-thumb { transform: translateX(16px); }",
|
|
45
|
+
'.lp-btn { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 4px; border: none; border-radius: var(--dsw-radius-md); cursor: pointer; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); background: transparent; padding: 0 14px; }',
|
|
46
|
+
'.lp-btn:disabled { cursor: not-allowed; opacity: 0.4; }',
|
|
47
|
+
'.lp-btn-sm { height: 28px; font-size: 12px; line-height: 18px; padding: 0 10px; border-radius: var(--dsw-radius-sm); }',
|
|
48
|
+
'.lp-btn-outline { border: 0.5px solid var(--dsw-alias-border-l3); background: transparent; }',
|
|
49
|
+
'.lp-btn-outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
50
|
+
].join('\n')
|
|
51
|
+
|
|
52
|
+
/** Rendered inside the component tree, so unmounting removes the styles. */
|
|
53
|
+
function Controls() {
|
|
54
|
+
return React.createElement('style', null, CONTROLS_CSS)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Switch(props) {
|
|
58
|
+
return React.createElement(
|
|
59
|
+
'button',
|
|
60
|
+
{
|
|
61
|
+
type: 'button',
|
|
62
|
+
role: 'switch',
|
|
63
|
+
'aria-checked': props.checked === true,
|
|
64
|
+
'aria-label': props.label,
|
|
65
|
+
className: 'lp-switch',
|
|
66
|
+
disabled: props.disabled === true,
|
|
67
|
+
onClick: () => {
|
|
68
|
+
if (props.disabled !== true) props.onChange(!(props.checked === true))
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
React.createElement('span', { className: 'lp-switch-thumb' }),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Switch.displayName = 'dsh-logicprobe:Switch'
|
|
76
|
+
Button.displayName = 'dsh-logicprobe:Button'
|
|
77
|
+
|
|
78
|
+
function Button(props) {
|
|
79
|
+
const classes = ['lp-btn']
|
|
80
|
+
if (props.size === 'sm') classes.push('lp-btn-sm')
|
|
81
|
+
if (props.variant === 'outline') classes.push('lp-btn-outline')
|
|
82
|
+
return React.createElement(
|
|
83
|
+
'button',
|
|
84
|
+
{ type: 'button', className: classes.join(' '), disabled: props.disabled === true, onClick: props.onClick },
|
|
85
|
+
props.children,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
34
88
|
|
|
35
89
|
/** Settings namespace: the Loader entry id this bundle's patch declares. */
|
|
36
90
|
const NS = 'logicprobe'
|
|
@@ -122,6 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
122
176
|
const locked = state.writable !== true || pending
|
|
123
177
|
|
|
124
178
|
const children = [
|
|
179
|
+
React.createElement(Controls, { key: 'styles' }),
|
|
125
180
|
React.createElement('h4', { key: 'title', style: TITLE }, t('title')),
|
|
126
181
|
React.createElement('div', { key: 'row', style: ROW }, [
|
|
127
182
|
React.createElement('span', { key: 'label', style: LABEL }, t('label')),
|