@andresmassello/uscha 1.95.0 → 1.97.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.md +72 -70
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +169 -14
- package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-status/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +2 -0
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/skills/uscha-adr-refine/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-characterize/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +169 -14
- package/uscha-kit/skills/uscha-discovery/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-mirador/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-rubric/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-status/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-sysdoc/SKILL.md +2 -0
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -40,81 +40,12 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
|
|
|
40
40
|
runtime dependencies). The npm package is a thin router; the canonical installer is
|
|
41
41
|
`uscha-kit/install-uscha.py`.
|
|
42
42
|
|
|
43
|
-
**Kit v1.
|
|
43
|
+
**Kit v1.97.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
-
## The diamond — specs are the source code, end to end
|
|
50
|
-
|
|
51
|
-
Uscha closes a cycle most spec-driven tools only walk halfway. The **spec package plus its
|
|
52
|
-
behavior ledger** is the canonical asset of a system; the code is a regenerable build artifact.
|
|
53
|
-
An LLM compiles the package into code under a validated contract; reverse discovery decompiles
|
|
54
|
-
existing code back into *curated* specs — passing, mandatorily, through the one step no
|
|
55
|
-
automatic tool can perform: a human verdict.
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
THE ASSET (solid) ── it appreciates with every model generation
|
|
59
|
-
┌───────────────────────────────────────────────────────────────┐
|
|
60
|
-
│ SPEC PACKAGE + BEHAVIOR LEDGER + IR │
|
|
61
|
-
│ SPEC · ADRs · ACCEPTANCE · CONSTITUTION │
|
|
62
|
-
│ verdicts: preserve · fix · undefined │
|
|
63
|
-
└──────────────┬─────────────────────────────▲──────────────────┘
|
|
64
|
-
│ │
|
|
65
|
-
FORWARD │ │ REVERSE
|
|
66
|
-
the LLM compiles │ │ reverse discovery
|
|
67
|
-
▼ │
|
|
68
|
-
┌─────────────────────────────────────┐ ┌─────────┴─────────────────────┐
|
|
69
|
-
│ compile-validate — output contract, │ │ CURATION · the human gate │
|
|
70
|
-
│ mechanical only, model-blind │ │ candidate ─▶ verdict ─▶ ledger │
|
|
71
|
-
│ withheld ORACLE — authored BEFORE │ │ no verdict → PR blocked, │
|
|
72
|
-
│ compiling, never in the prompt │ │ the candidate is NAMED │
|
|
73
|
-
└─────────────────┬───────────────────┘ └─────────▲─────────────────────┘
|
|
74
|
-
│ │
|
|
75
|
-
▼ │
|
|
76
|
-
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┴─────────────────────┐
|
|
77
|
-
CODE (dashed) — build artifact, │ discover · golden capture │
|
|
78
|
-
regenerable, disposable ───▶│ candidates: typed evidence │
|
|
79
|
-
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ + confidence │
|
|
80
|
-
└───────────────────────────────────┘
|
|
81
|
-
▲ also enters here: any LEGACY
|
|
82
|
-
system (= 100% drift)
|
|
83
|
-
|
|
84
|
-
round trip · bench-roundtrip — how much of the asset the reverse organs re-anchor
|
|
85
|
-
from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**What each arrow is, in the engine (kit 1.95.0, 53 subcommands, all measured):**
|
|
89
|
-
|
|
90
|
-
| Leg | Subcommands | What it establishes |
|
|
91
|
-
|---|---|---|
|
|
92
|
-
| Asset → typed graph | `ir-extract`, `ir-render` | the whole package becomes one canonical IR (M2, ADR-015) — deterministic, `UNTYPED` is a measurement not an error |
|
|
93
|
-
| Forward, the compiler | `compile-validate`, `compile-ingest` | any model produces code; the engine validates the output contract and never compiles (M3, ADR-016) |
|
|
94
|
-
| Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 9 PASS · 3 PARTIAL**, three Claude-family models (Haiku · Sonnet · Opus — one vendor; cross-vendor not yet measured), JS included (M4/M5, ADR-017/018/028/029) |
|
|
95
|
-
| Reverse, facts | `discover`, `golden-diff` (+ the `/uscha-characterize` skill) | system map + mechanically captured golden; typed candidate observations with evidence class (M1, ADR-013) |
|
|
96
|
-
| Reverse, the human gate | `curate`, `promote`, `curation-check`, `bench-curate` | one verdict per candidate, append-only ledger verified against git; unjudged → `pr-ready` blocked naming it (ADR-009/010, INV-CURATION-01) |
|
|
97
|
-
| Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.828**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
|
|
98
|
-
|
|
99
|
-
**Read the numbers the way the repo does.** 9 of 12 archetypes regenerate to the same system
|
|
100
|
-
under an oracle the compilers never saw — that is the closed loop working. 0.828 is the mean
|
|
101
|
-
*recoverability* of the asset from compiled code counting only static and behavioural
|
|
102
|
-
footing. It read **0.062** until 1.90.0, with the behaviour dimension `UNMEASURED`, because no
|
|
103
|
-
oracle case carried an AC tag — a named absence, not a zero. The 12 bench oracles are now
|
|
104
|
-
curated per case (`ORACLE-TAGS-CURATED.json`, human-authored; payloads and expectations
|
|
105
|
-
untouched), so the dimension is measured and the number says the reverse organs anchor **names
|
|
106
|
-
and behaviour**. Both numbers are published rather than smoothed, and what moved between them
|
|
107
|
-
was the tagging, not the code under test. And `bench-r2` measured
|
|
108
|
-
that same-model reruns differ structurally about as much as different models do (aggregate
|
|
109
|
-
`NOISY`) — so one earlier variance narrative was **retracted**. Every claim above is a subcommand
|
|
110
|
-
you can run; every unmeasured part is labeled. That honesty is the method applied to itself.
|
|
111
|
-
|
|
112
|
-
→ The full thesis, with before/after diagrams and the REAL vs VISION table:
|
|
113
|
-
**[uscha.dev/diamond](https://uscha.dev/diamond)** · the mechanism, in three diagrams:
|
|
114
|
-
**[uscha.dev/how](https://uscha.dev/how)**
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
49
|
## The problem it solves
|
|
119
50
|
|
|
120
51
|
An agent will tell you the tests pass. It will tell you the feature is done. It is often
|
|
@@ -202,6 +133,75 @@ else `doctor` reports `advisory` rather than implying a guard it cannot see.
|
|
|
202
133
|
- **Model-agnostic.** The engine never reads tokens, model names or vendor telemetry. Any
|
|
203
134
|
model-reported number enters through an adapter, never the engine.
|
|
204
135
|
|
|
136
|
+
## The diamond — specs are the source code, end to end
|
|
137
|
+
|
|
138
|
+
Uscha closes a cycle most spec-driven tools only walk halfway. The **spec package plus its
|
|
139
|
+
behavior ledger** is the canonical asset of a system; the code is a regenerable build artifact.
|
|
140
|
+
An LLM compiles the package into code under a validated contract; reverse discovery decompiles
|
|
141
|
+
existing code back into *curated* specs — passing, mandatorily, through the one step no
|
|
142
|
+
automatic tool can perform: a human verdict.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
THE ASSET (solid) ── it appreciates with every model generation
|
|
146
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
147
|
+
│ SPEC PACKAGE + BEHAVIOR LEDGER + IR │
|
|
148
|
+
│ SPEC · ADRs · ACCEPTANCE · CONSTITUTION │
|
|
149
|
+
│ verdicts: preserve · fix · undefined │
|
|
150
|
+
└──────────────┬─────────────────────────────▲──────────────────┘
|
|
151
|
+
│ │
|
|
152
|
+
FORWARD │ │ REVERSE
|
|
153
|
+
the LLM compiles │ │ reverse discovery
|
|
154
|
+
▼ │
|
|
155
|
+
┌─────────────────────────────────────┐ ┌─────────┴─────────────────────┐
|
|
156
|
+
│ compile-validate — output contract, │ │ CURATION · the human gate │
|
|
157
|
+
│ mechanical only, model-blind │ │ candidate ─▶ verdict ─▶ ledger │
|
|
158
|
+
│ withheld ORACLE — authored BEFORE │ │ no verdict → PR blocked, │
|
|
159
|
+
│ compiling, never in the prompt │ │ the candidate is NAMED │
|
|
160
|
+
└─────────────────┬───────────────────┘ └─────────▲─────────────────────┘
|
|
161
|
+
│ │
|
|
162
|
+
▼ │
|
|
163
|
+
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┴─────────────────────┐
|
|
164
|
+
CODE (dashed) — build artifact, │ discover · golden capture │
|
|
165
|
+
regenerable, disposable ───▶│ candidates: typed evidence │
|
|
166
|
+
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ + confidence │
|
|
167
|
+
└───────────────────────────────────┘
|
|
168
|
+
▲ also enters here: any LEGACY
|
|
169
|
+
system (= 100% drift)
|
|
170
|
+
|
|
171
|
+
round trip · bench-roundtrip — how much of the asset the reverse organs re-anchor
|
|
172
|
+
from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**What each arrow is, in the engine (kit 1.96.0, 53 subcommands, all measured):**
|
|
176
|
+
|
|
177
|
+
| Leg | Subcommands | What it establishes |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| Asset → typed graph | `ir-extract`, `ir-render` | the whole package becomes one canonical IR (M2, ADR-015) — deterministic, `UNTYPED` is a measurement not an error |
|
|
180
|
+
| Forward, the compiler | `compile-validate`, `compile-ingest` | any model produces code; the engine validates the output contract and never compiles (M3, ADR-016) |
|
|
181
|
+
| Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 9 PASS · 3 PARTIAL**, three Claude-family models (Haiku · Sonnet · Opus — one vendor; cross-vendor not yet measured), JS included (M4/M5, ADR-017/018/028/029) |
|
|
182
|
+
| Reverse, facts | `discover`, `golden-diff` (+ the `/uscha-characterize` skill) | system map + mechanically captured golden; typed candidate observations with evidence class (M1, ADR-013) |
|
|
183
|
+
| Reverse, the human gate | `curate`, `promote`, `curation-check`, `bench-curate` | one verdict per candidate, append-only ledger verified against git; unjudged → `pr-ready` blocked naming it (ADR-009/010, INV-CURATION-01) |
|
|
184
|
+
| Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.828**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
|
|
185
|
+
|
|
186
|
+
**Read the numbers the way the repo does.** 9 of 12 archetypes regenerate to the same system
|
|
187
|
+
under an oracle the compilers never saw — that is the closed loop working. 0.828 is the mean
|
|
188
|
+
*recoverability* of the asset from compiled code counting only static and behavioural
|
|
189
|
+
footing. It read **0.062** until 1.90.0, with the behaviour dimension `UNMEASURED`, because no
|
|
190
|
+
oracle case carried an AC tag — a named absence, not a zero. The 12 bench oracles are now
|
|
191
|
+
curated per case (`ORACLE-TAGS-CURATED.json`, human-authored; payloads and expectations
|
|
192
|
+
untouched), so the dimension is measured and the number says the reverse organs anchor **names
|
|
193
|
+
and behaviour**. Both numbers are published rather than smoothed, and what moved between them
|
|
194
|
+
was the tagging, not the code under test. And `bench-r2` measured
|
|
195
|
+
that same-model reruns differ structurally about as much as different models do (aggregate
|
|
196
|
+
`NOISY`) — so one earlier variance narrative was **retracted**. Every claim above is a subcommand
|
|
197
|
+
you can run; every unmeasured part is labeled. That honesty is the method applied to itself.
|
|
198
|
+
|
|
199
|
+
→ The full thesis, with before/after diagrams and the REAL vs VISION table:
|
|
200
|
+
**[uscha.dev/diamond](https://uscha.dev/diamond)** · the mechanism, in three diagrams:
|
|
201
|
+
**[uscha.dev/how](https://uscha.dev/how)**
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
205
|
## Documentation
|
|
206
206
|
|
|
207
207
|
- **[`uscha-kit/INSTALL.md`](uscha-kit/INSTALL.md)** — full install guide (npm, git, plugin)
|
|
@@ -209,6 +209,8 @@ else `doctor` reports `advisory` rather than implying a guard it cannot see.
|
|
|
209
209
|
subcommand, the readiness KPI, the simplicity and rebuild gates
|
|
210
210
|
- **[`docs/`](docs/)** — the long deck, the operator's playbook, a skills reference and a
|
|
211
211
|
one-pager (ES + EN)
|
|
212
|
+
- **[`docs/adr/INDEX.md`](docs/adr/INDEX.md)** — every architecture decision, grouped by who
|
|
213
|
+
needs it, with a five-ADR reading order for a newcomer
|
|
212
214
|
- **[`docs/paper/`](docs/paper/)** — the method written up as a paper
|
|
213
215
|
|
|
214
216
|
Each release ships a `uscha-kit/CHANGELOG-X.Y.Z.md` explaining what changed and why.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.97.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -18,6 +18,7 @@ You convert a rough idea into a development-ready specification. You do this in
|
|
|
18
18
|
phases. **You are NOT a generator. You are an interrogator that distills.** The value
|
|
19
19
|
is in the questions, not in agreeing.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Non-negotiable principles
|
|
90
92
|
|
|
@@ -18,6 +18,7 @@ return, you encode the same partial understanding that loses logic silently. **Y
|
|
|
18
18
|
what the code DOES, mechanically, by running it — never what it should do.** You may write
|
|
19
19
|
the capture harness; you may NOT create, rename, or edit any `.approved` file.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Inputs
|
|
90
92
|
|
|
@@ -26,6 +26,7 @@ two tiers: **measured** records (snapshots, ingest-gate, log-gate — parsed fro
|
|
|
26
26
|
artifacts; these can block) and **self-reported** agent counts (log-step — narration
|
|
27
27
|
recorded for the retrospective; a measured red always overrides a narrated green).
|
|
28
28
|
|
|
29
|
+
<!-- uscha:orientation-block:begin -->
|
|
29
30
|
## First contact (show ONCE, then never again)
|
|
30
31
|
|
|
31
32
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -93,6 +94,7 @@ and say exactly what unblocks it.
|
|
|
93
94
|
|
|
94
95
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
95
96
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
97
|
+
<!-- uscha:orientation-block:end -->
|
|
96
98
|
|
|
97
99
|
## Non-negotiable principles
|
|
98
100
|
|
|
@@ -7251,13 +7251,46 @@ def _derive_facts():
|
|
|
7251
7251
|
}
|
|
7252
7252
|
|
|
7253
7253
|
|
|
7254
|
+
# Spelled-out counts are claims too, and the paper writes both forms in one sentence -- "nine
|
|
7255
|
+
# agent skills and a dependency-free Python engine with 53 subcommands". A gate that only sees
|
|
7256
|
+
# digits reads half of that sentence and calls the file green. 1..99 covers every count this repo
|
|
7257
|
+
# derives, with room; above it the number is written in digits everywhere it appears.
|
|
7258
|
+
_ONES = ("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
7259
|
+
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
|
|
7260
|
+
"seventeen", "eighteen", "nineteen")
|
|
7261
|
+
_TENS = ("", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety")
|
|
7262
|
+
|
|
7263
|
+
|
|
7264
|
+
def _spell(n):
|
|
7265
|
+
"""1..99 in English words, hyphenated ("fifty-three"); None outside that range."""
|
|
7266
|
+
if not 1 <= n <= 99:
|
|
7267
|
+
return None
|
|
7268
|
+
if n < 20:
|
|
7269
|
+
return _ONES[n]
|
|
7270
|
+
return _TENS[n // 10] + ("-" + _ONES[n % 10] if n % 10 else "")
|
|
7271
|
+
|
|
7272
|
+
|
|
7273
|
+
_SPELLED = dict((_spell(n), n) for n in range(1, 100))
|
|
7274
|
+
# longest alternative first: an alternation offering "six" before "sixty-three" matches the prefix
|
|
7275
|
+
_NUM_ALT = "|".join(sorted(_SPELLED, key=len, reverse=True))
|
|
7276
|
+
# the leading \b so that "someone skills" cannot be read as the claim "one skills"
|
|
7277
|
+
_COUNT = r"\b(\d+|" + _NUM_ALT + r")\s+"
|
|
7278
|
+
|
|
7279
|
+
|
|
7280
|
+
def _unspell(token):
|
|
7281
|
+
"""The integer a spelled-out count names, or None when the token is not one."""
|
|
7282
|
+
return _SPELLED.get(token.lower())
|
|
7283
|
+
|
|
7284
|
+
|
|
7254
7285
|
_CLAIM_PATTERNS = (
|
|
7255
7286
|
# (fact key path, regex over one line, needs-context substring or None)
|
|
7256
7287
|
("version", r"v(\d+\.\d+\.\d+)", "kit"),
|
|
7257
7288
|
("version", r"uscha-kit\s+v?(\d+\.\d+\.\d+)", None),
|
|
7258
|
-
("subcommands.count", r"
|
|
7289
|
+
("subcommands.count", _COUNT + r"sub-?comm?ands", None),
|
|
7259
7290
|
("subcommands.count", r"(\d+)\s+subcomandos", None),
|
|
7260
|
-
|
|
7291
|
+
# "agent skills" is the kit's own noun phrase and the paper's; nothing wider is let in,
|
|
7292
|
+
# because a WRITER that guessed at "two other skills" would corrupt the sentence it fixed.
|
|
7293
|
+
("skills.count", _COUNT + r"(?:agent\s+)?skills", None),
|
|
7261
7294
|
)
|
|
7262
7295
|
|
|
7263
7296
|
|
|
@@ -7268,6 +7301,110 @@ def _fact_value(facts, dotted):
|
|
|
7268
7301
|
return str(cur)
|
|
7269
7302
|
|
|
7270
7303
|
|
|
7304
|
+
def _claim_norm(token):
|
|
7305
|
+
"""The comparable form of a claimed token: a spelled-out count normalises to its digits, so
|
|
7306
|
+
"nine skills" and "9 skills" are one claim compared against one fact."""
|
|
7307
|
+
n = _unspell(token)
|
|
7308
|
+
return token if n is None else str(n)
|
|
7309
|
+
|
|
7310
|
+
|
|
7311
|
+
def _claim_rewrite(token, actual):
|
|
7312
|
+
"""The replacement for one claimed token, in the AUTHOR's notation: a spelled-out claim is
|
|
7313
|
+
rewritten spelled out and keeps its leading capital, a numeric one numerically. Rewriting
|
|
7314
|
+
"nine skills" as "10 skills" would fix the fact and break the sentence."""
|
|
7315
|
+
if _unspell(token) is None:
|
|
7316
|
+
return actual
|
|
7317
|
+
word = _spell(int(actual)) if actual.isdigit() else None
|
|
7318
|
+
if word is None:
|
|
7319
|
+
return actual
|
|
7320
|
+
return word[0].upper() + word[1:] if token[:1].isupper() else word
|
|
7321
|
+
|
|
7322
|
+
|
|
7323
|
+
def _iter_claims(line):
|
|
7324
|
+
"""Every recognised claim on ONE line, as (fact key, start, end, token).
|
|
7325
|
+
|
|
7326
|
+
ONE recogniser, two consumers: `--check` reports what this yields and `--write` rewrites
|
|
7327
|
+
exactly what this yields. A writer that re-implemented the patterns could disagree with the
|
|
7328
|
+
checker, and the disagreement would surface as a release that refuses after fixing itself.
|
|
7329
|
+
|
|
7330
|
+
HTML comment spans are SKIPPED rather than deleted -- a comment is not a published claim
|
|
7331
|
+
(the first live run flagged a section marker reading "2 Skills"), and `--write` needs offsets
|
|
7332
|
+
into the ORIGINAL line. Overlapping matches collapse: the two version patterns can name the
|
|
7333
|
+
same digits, and rewriting one span twice would corrupt the line."""
|
|
7334
|
+
spans = [m.span() for m in re.finditer(r"<!--.*?-->", line)]
|
|
7335
|
+
low = line.lower()
|
|
7336
|
+
found = []
|
|
7337
|
+
for key, pat, ctx in _CLAIM_PATTERNS:
|
|
7338
|
+
if ctx and ctx not in low:
|
|
7339
|
+
continue
|
|
7340
|
+
for m in re.finditer(pat, line, re.I):
|
|
7341
|
+
if any(a <= m.start() < b for a, b in spans):
|
|
7342
|
+
continue
|
|
7343
|
+
found.append((m.start(1), m.end(1), key, m.group(1)))
|
|
7344
|
+
out, taken = [], []
|
|
7345
|
+
for start, end, key, token in sorted(found):
|
|
7346
|
+
if any(start < e and s < end for s, e in taken):
|
|
7347
|
+
continue
|
|
7348
|
+
taken.append((start, end))
|
|
7349
|
+
out.append((key, start, end, token))
|
|
7350
|
+
return out
|
|
7351
|
+
|
|
7352
|
+
|
|
7353
|
+
def _write_claims(facts, paths):
|
|
7354
|
+
"""Rewrite every recognised STALE claim in `paths` to the derived fact, byte for byte
|
|
7355
|
+
otherwise. Line endings are preserved PER FILE (newline="" on both ends): the gated set mixes
|
|
7356
|
+
LF sources with CRLF-checked-out HTML, and normalising them would turn a one-token fix into a
|
|
7357
|
+
whole-file diff nobody can review.
|
|
7358
|
+
|
|
7359
|
+
What it deliberately does NOT touch: anything the patterns do not recognise. A missing
|
|
7360
|
+
subcommand table row, a count spelled outside 1..99, a claim phrased in prose -- those stay
|
|
7361
|
+
for the human, and the --check that follows still fails on them."""
|
|
7362
|
+
changed, total, problems = 0, 0, 0
|
|
7363
|
+
for path in paths:
|
|
7364
|
+
try:
|
|
7365
|
+
with open(path, encoding="utf-8", newline="") as fh:
|
|
7366
|
+
body = fh.read()
|
|
7367
|
+
except OSError as exc:
|
|
7368
|
+
print(" !! %s: unreadable: %s" % (path, exc))
|
|
7369
|
+
problems += 1
|
|
7370
|
+
continue
|
|
7371
|
+
except UnicodeDecodeError as exc:
|
|
7372
|
+
# `--check` reads this file with errors="replace" and still reports its claims, so
|
|
7373
|
+
# nothing is hidden. `--write` must NOT read it that way: writing a replaced byte
|
|
7374
|
+
# back would destroy data to fix a version number, and a writer that corrupts a file
|
|
7375
|
+
# to correct a claim is worse than the claim. Named, skipped, and the --check that
|
|
7376
|
+
# follows still fails on whatever is stale in it.
|
|
7377
|
+
print(" !! %s: not valid UTF-8 (%s) -- left untouched; --check still reads it"
|
|
7378
|
+
% (path, exc))
|
|
7379
|
+
problems += 1
|
|
7380
|
+
continue
|
|
7381
|
+
lines = body.split("\n")
|
|
7382
|
+
n = 0
|
|
7383
|
+
for i, line in enumerate(lines):
|
|
7384
|
+
claims = _iter_claims(line)
|
|
7385
|
+
if not claims:
|
|
7386
|
+
continue
|
|
7387
|
+
# right to left: an earlier rewrite must not move the offsets of a later one
|
|
7388
|
+
for key, start, end, token in sorted(claims, key=lambda c: c[1], reverse=True):
|
|
7389
|
+
actual = _fact_value(facts, key)
|
|
7390
|
+
if _claim_norm(token) == actual:
|
|
7391
|
+
continue
|
|
7392
|
+
line = line[:start] + _claim_rewrite(token, actual) + line[end:]
|
|
7393
|
+
n += 1
|
|
7394
|
+
lines[i] = line
|
|
7395
|
+
if not n:
|
|
7396
|
+
continue
|
|
7397
|
+
with open(path, "w", encoding="utf-8", newline="") as fh:
|
|
7398
|
+
fh.write("\n".join(lines))
|
|
7399
|
+
print("%s: %d claim(s) rewritten" % (path, n))
|
|
7400
|
+
changed += 1
|
|
7401
|
+
total += n
|
|
7402
|
+
print("FACTS --write: %d claim(s) rewritten in %d of %d file(s)%s"
|
|
7403
|
+
% (total, changed, len(paths),
|
|
7404
|
+
"" if not problems else "; %d file(s) could not be read" % problems))
|
|
7405
|
+
return problems
|
|
7406
|
+
|
|
7407
|
+
|
|
7271
7408
|
def cmd_facts(args):
|
|
7272
7409
|
"""Generate SYSTEM-FACTS.json, or --check published claims against the derived facts.
|
|
7273
7410
|
|
|
@@ -7276,6 +7413,24 @@ def cmd_facts(args):
|
|
|
7276
7413
|
about factual drift. A claim that CI does not compare against a derived fact will
|
|
7277
7414
|
drift; this makes the comparison mechanical and the drift a named red."""
|
|
7278
7415
|
facts = _derive_facts()
|
|
7416
|
+
if args.write is not None:
|
|
7417
|
+
# Until 1.97.0 there was no writer, so every bump was ~25 hand edits across ~13 files and
|
|
7418
|
+
# the release script could only refuse and hand them back. `--write` rewrites the claims
|
|
7419
|
+
# the SAME recogniser finds, then runs the SAME --check: a writer that reported its own
|
|
7420
|
+
# success would be exactly the self-graded evidence this engine exists to refuse.
|
|
7421
|
+
if args.check is not None:
|
|
7422
|
+
# Before 1.97.0 this combination silently dropped --check's files: --write set
|
|
7423
|
+
# args.check to ITS list. Two file sets given, one measured, no word about it.
|
|
7424
|
+
print("[qa_ledger] facts: --check and --write are alternatives, not a pair -- "
|
|
7425
|
+
"--write already re-checks exactly the files it wrote.", file=sys.stderr)
|
|
7426
|
+
sys.exit(2)
|
|
7427
|
+
if not args.write:
|
|
7428
|
+
print("[qa_ledger] facts: --write needs at least one file.", file=sys.stderr)
|
|
7429
|
+
sys.exit(2)
|
|
7430
|
+
if _write_claims(facts, args.write):
|
|
7431
|
+
# an unreadable file is an UNMEASURED claim set, and unmeasured is not green
|
|
7432
|
+
sys.exit(2)
|
|
7433
|
+
args.check = list(args.write)
|
|
7279
7434
|
if args.check:
|
|
7280
7435
|
problems = []
|
|
7281
7436
|
# 1) the committed facts file must match a fresh derivation (stale facts are drift)
|
|
@@ -7301,21 +7456,17 @@ def cmd_facts(args):
|
|
|
7301
7456
|
table_names = []
|
|
7302
7457
|
table_start = 0
|
|
7303
7458
|
for n, line in enumerate(lines, 1):
|
|
7304
|
-
#
|
|
7305
|
-
#
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
continue
|
|
7311
|
-
for m in re.finditer(pat, line, re.I):
|
|
7312
|
-
claimed = m.group(1)
|
|
7313
|
-
actual = _fact_value(facts, key)
|
|
7314
|
-
if claimed != actual:
|
|
7315
|
-
problems.append((path, n, key, claimed, actual))
|
|
7459
|
+
# the claims come from the SHARED recogniser (`_iter_claims`), which skips HTML
|
|
7460
|
+
# comment spans -- a comment is not a published claim
|
|
7461
|
+
for key, _s, _e, claimed in _iter_claims(line):
|
|
7462
|
+
actual = _fact_value(facts, key)
|
|
7463
|
+
if _claim_norm(claimed) != actual:
|
|
7464
|
+
problems.append((path, n, key, claimed, actual))
|
|
7316
7465
|
# the parser-surface table (Subcommand/Subcomando header, one `<td class="t">`
|
|
7317
7466
|
# row per subcommand) is a claim too, just not a numeric one -- a row can go
|
|
7318
7467
|
# missing while the count beside it stays correct (the `top` row did, once).
|
|
7468
|
+
# This half reads the comment-STRIPPED line: a commented-out row is not a row.
|
|
7469
|
+
line = re.sub(r"<!--.*?-->", "", line)
|
|
7319
7470
|
if not in_table:
|
|
7320
7471
|
if re.search(r"<th>Sub ?comm?ando?s?</th>", line, re.I):
|
|
7321
7472
|
in_table, table_names, table_start = True, [], n
|
|
@@ -12288,6 +12439,10 @@ def build_parser():
|
|
|
12288
12439
|
pfa.add_argument("--out", default="SYSTEM-FACTS.json")
|
|
12289
12440
|
pfa.add_argument("--check", nargs="*", default=None,
|
|
12290
12441
|
help="files whose claims must match the derived facts; exit 1 on drift")
|
|
12442
|
+
pfa.add_argument("--write", nargs="*", default=None,
|
|
12443
|
+
help="files whose recognised claims are REWRITTEN to the derived facts "
|
|
12444
|
+
"(spelled-out claims stay spelled out), then re-checked: exit 1 if "
|
|
12445
|
+
"anything still disagrees")
|
|
12291
12446
|
pfa.set_defaults(func=cmd_facts)
|
|
12292
12447
|
|
|
12293
12448
|
prt = sub.add_parser("roundtrip",
|
|
@@ -18,6 +18,7 @@ The human brings the idea, the constraints and the reference material. **You bri
|
|
|
18
18
|
shape.** Your job is to interrogate until there is a shared system shape, and to write
|
|
19
19
|
the documents as you go — not to ask the human to design the system for you.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Non-negotiable principles
|
|
90
92
|
|
|
@@ -17,6 +17,7 @@ allowed-tools: Read, Write, Glob, Grep, Bash
|
|
|
17
17
|
Paints the REAL state of the project at a glance. It does not narrate or estimate: it
|
|
18
18
|
wires the JSON the engine emits into the template. Read-only.
|
|
19
19
|
|
|
20
|
+
<!-- uscha:orientation-block:begin -->
|
|
20
21
|
## Orientation markers (non-negotiable)
|
|
21
22
|
|
|
22
23
|
The operator must never have to ask "where am I?" or "what happens now?".
|
|
@@ -40,6 +41,7 @@ Run: <the exact command or skill to invoke>
|
|
|
40
41
|
including any `Flow:` line in this file. If nothing is actionable, say that plainly rather
|
|
41
42
|
than inventing a step. Keep the CONTENT in the conversation's language and the labels
|
|
42
43
|
(`Next`, `Run`) verbatim — the smoke suite checks for them.
|
|
44
|
+
<!-- uscha:orientation-block:end -->
|
|
43
45
|
|
|
44
46
|
## Contract
|
|
45
47
|
|
|
@@ -23,6 +23,7 @@ first, always — and what cannot be fact yet becomes an OBSERVATION in quaranti
|
|
|
23
23
|
evidence-classed, content-addressed, and promoted to the contract only by a per-observation
|
|
24
24
|
human verdict (ADR-013).**
|
|
25
25
|
|
|
26
|
+
<!-- uscha:orientation-block:begin -->
|
|
26
27
|
## First contact (show ONCE, then never again)
|
|
27
28
|
|
|
28
29
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -92,6 +93,7 @@ and say exactly what unblocks it.
|
|
|
92
93
|
|
|
93
94
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
94
95
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
96
|
+
<!-- uscha:orientation-block:end -->
|
|
95
97
|
|
|
96
98
|
## The one non-negotiable: quarantine, not judgment (ADR-009)
|
|
97
99
|
|
|
@@ -21,6 +21,7 @@ contract + `qa_ledger.py rubric-ingest` (stdlib, runs anywhere). ANY runner can
|
|
|
21
21
|
the grader — this skill just wraps the neutral prompt so Claude Code users get it
|
|
22
22
|
in one command. Never add Claude-specific behavior to the contract.
|
|
23
23
|
|
|
24
|
+
<!-- uscha:orientation-block:begin -->
|
|
24
25
|
## First contact (show ONCE, then never again)
|
|
25
26
|
|
|
26
27
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -88,6 +89,7 @@ and say exactly what unblocks it.
|
|
|
88
89
|
|
|
89
90
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
90
91
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
92
|
+
<!-- uscha:orientation-block:end -->
|
|
91
93
|
|
|
92
94
|
## Protocol
|
|
93
95
|
|
|
@@ -19,6 +19,7 @@ skill (**pull** — one screen when the human asks), and the **mirador** (bird's
|
|
|
19
19
|
HTML). This skill exists because some surfaces never show a statusline; the answer
|
|
20
20
|
is the same data, printed in chat when requested.
|
|
21
21
|
|
|
22
|
+
<!-- uscha:orientation-block:begin -->
|
|
22
23
|
## Orientation markers (non-negotiable)
|
|
23
24
|
|
|
24
25
|
The operator must never have to ask "where am I?" or "what happens now?".
|
|
@@ -42,6 +43,7 @@ Run: <the exact command or skill to invoke>
|
|
|
42
43
|
including any `Flow:` line in this file. If nothing is actionable, say that plainly rather
|
|
43
44
|
than inventing a step. Keep the CONTENT in the conversation's language and the labels
|
|
44
45
|
(`Next`, `Run`) verbatim — the smoke suite checks for them.
|
|
46
|
+
<!-- uscha:orientation-block:end -->
|
|
45
47
|
|
|
46
48
|
## Contract
|
|
47
49
|
|
|
@@ -21,6 +21,7 @@ switch between at any time:
|
|
|
21
21
|
- **Technical track** — architecture, modules, data flow, contracts, QA results,
|
|
22
22
|
coverage, known deferred issues.
|
|
23
23
|
|
|
24
|
+
<!-- uscha:orientation-block:begin -->
|
|
24
25
|
## First contact (show ONCE, then never again)
|
|
25
26
|
|
|
26
27
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -88,6 +89,7 @@ and say exactly what unblocks it.
|
|
|
88
89
|
|
|
89
90
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
90
91
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
92
|
+
<!-- uscha:orientation-block:end -->
|
|
91
93
|
|
|
92
94
|
## Inputs
|
|
93
95
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.97.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
6
|
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.97.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.97.0
|
|
@@ -18,6 +18,7 @@ You convert a rough idea into a development-ready specification. You do this in
|
|
|
18
18
|
phases. **You are NOT a generator. You are an interrogator that distills.** The value
|
|
19
19
|
is in the questions, not in agreeing.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Non-negotiable principles
|
|
90
92
|
|
|
@@ -18,6 +18,7 @@ return, you encode the same partial understanding that loses logic silently. **Y
|
|
|
18
18
|
what the code DOES, mechanically, by running it — never what it should do.** You may write
|
|
19
19
|
the capture harness; you may NOT create, rename, or edit any `.approved` file.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Inputs
|
|
90
92
|
|
|
@@ -26,6 +26,7 @@ two tiers: **measured** records (snapshots, ingest-gate, log-gate — parsed fro
|
|
|
26
26
|
artifacts; these can block) and **self-reported** agent counts (log-step — narration
|
|
27
27
|
recorded for the retrospective; a measured red always overrides a narrated green).
|
|
28
28
|
|
|
29
|
+
<!-- uscha:orientation-block:begin -->
|
|
29
30
|
## First contact (show ONCE, then never again)
|
|
30
31
|
|
|
31
32
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -93,6 +94,7 @@ and say exactly what unblocks it.
|
|
|
93
94
|
|
|
94
95
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
95
96
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
97
|
+
<!-- uscha:orientation-block:end -->
|
|
96
98
|
|
|
97
99
|
## Non-negotiable principles
|
|
98
100
|
|
|
@@ -7251,13 +7251,46 @@ def _derive_facts():
|
|
|
7251
7251
|
}
|
|
7252
7252
|
|
|
7253
7253
|
|
|
7254
|
+
# Spelled-out counts are claims too, and the paper writes both forms in one sentence -- "nine
|
|
7255
|
+
# agent skills and a dependency-free Python engine with 53 subcommands". A gate that only sees
|
|
7256
|
+
# digits reads half of that sentence and calls the file green. 1..99 covers every count this repo
|
|
7257
|
+
# derives, with room; above it the number is written in digits everywhere it appears.
|
|
7258
|
+
_ONES = ("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
7259
|
+
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
|
|
7260
|
+
"seventeen", "eighteen", "nineteen")
|
|
7261
|
+
_TENS = ("", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety")
|
|
7262
|
+
|
|
7263
|
+
|
|
7264
|
+
def _spell(n):
|
|
7265
|
+
"""1..99 in English words, hyphenated ("fifty-three"); None outside that range."""
|
|
7266
|
+
if not 1 <= n <= 99:
|
|
7267
|
+
return None
|
|
7268
|
+
if n < 20:
|
|
7269
|
+
return _ONES[n]
|
|
7270
|
+
return _TENS[n // 10] + ("-" + _ONES[n % 10] if n % 10 else "")
|
|
7271
|
+
|
|
7272
|
+
|
|
7273
|
+
_SPELLED = dict((_spell(n), n) for n in range(1, 100))
|
|
7274
|
+
# longest alternative first: an alternation offering "six" before "sixty-three" matches the prefix
|
|
7275
|
+
_NUM_ALT = "|".join(sorted(_SPELLED, key=len, reverse=True))
|
|
7276
|
+
# the leading \b so that "someone skills" cannot be read as the claim "one skills"
|
|
7277
|
+
_COUNT = r"\b(\d+|" + _NUM_ALT + r")\s+"
|
|
7278
|
+
|
|
7279
|
+
|
|
7280
|
+
def _unspell(token):
|
|
7281
|
+
"""The integer a spelled-out count names, or None when the token is not one."""
|
|
7282
|
+
return _SPELLED.get(token.lower())
|
|
7283
|
+
|
|
7284
|
+
|
|
7254
7285
|
_CLAIM_PATTERNS = (
|
|
7255
7286
|
# (fact key path, regex over one line, needs-context substring or None)
|
|
7256
7287
|
("version", r"v(\d+\.\d+\.\d+)", "kit"),
|
|
7257
7288
|
("version", r"uscha-kit\s+v?(\d+\.\d+\.\d+)", None),
|
|
7258
|
-
("subcommands.count", r"
|
|
7289
|
+
("subcommands.count", _COUNT + r"sub-?comm?ands", None),
|
|
7259
7290
|
("subcommands.count", r"(\d+)\s+subcomandos", None),
|
|
7260
|
-
|
|
7291
|
+
# "agent skills" is the kit's own noun phrase and the paper's; nothing wider is let in,
|
|
7292
|
+
# because a WRITER that guessed at "two other skills" would corrupt the sentence it fixed.
|
|
7293
|
+
("skills.count", _COUNT + r"(?:agent\s+)?skills", None),
|
|
7261
7294
|
)
|
|
7262
7295
|
|
|
7263
7296
|
|
|
@@ -7268,6 +7301,110 @@ def _fact_value(facts, dotted):
|
|
|
7268
7301
|
return str(cur)
|
|
7269
7302
|
|
|
7270
7303
|
|
|
7304
|
+
def _claim_norm(token):
|
|
7305
|
+
"""The comparable form of a claimed token: a spelled-out count normalises to its digits, so
|
|
7306
|
+
"nine skills" and "9 skills" are one claim compared against one fact."""
|
|
7307
|
+
n = _unspell(token)
|
|
7308
|
+
return token if n is None else str(n)
|
|
7309
|
+
|
|
7310
|
+
|
|
7311
|
+
def _claim_rewrite(token, actual):
|
|
7312
|
+
"""The replacement for one claimed token, in the AUTHOR's notation: a spelled-out claim is
|
|
7313
|
+
rewritten spelled out and keeps its leading capital, a numeric one numerically. Rewriting
|
|
7314
|
+
"nine skills" as "10 skills" would fix the fact and break the sentence."""
|
|
7315
|
+
if _unspell(token) is None:
|
|
7316
|
+
return actual
|
|
7317
|
+
word = _spell(int(actual)) if actual.isdigit() else None
|
|
7318
|
+
if word is None:
|
|
7319
|
+
return actual
|
|
7320
|
+
return word[0].upper() + word[1:] if token[:1].isupper() else word
|
|
7321
|
+
|
|
7322
|
+
|
|
7323
|
+
def _iter_claims(line):
|
|
7324
|
+
"""Every recognised claim on ONE line, as (fact key, start, end, token).
|
|
7325
|
+
|
|
7326
|
+
ONE recogniser, two consumers: `--check` reports what this yields and `--write` rewrites
|
|
7327
|
+
exactly what this yields. A writer that re-implemented the patterns could disagree with the
|
|
7328
|
+
checker, and the disagreement would surface as a release that refuses after fixing itself.
|
|
7329
|
+
|
|
7330
|
+
HTML comment spans are SKIPPED rather than deleted -- a comment is not a published claim
|
|
7331
|
+
(the first live run flagged a section marker reading "2 Skills"), and `--write` needs offsets
|
|
7332
|
+
into the ORIGINAL line. Overlapping matches collapse: the two version patterns can name the
|
|
7333
|
+
same digits, and rewriting one span twice would corrupt the line."""
|
|
7334
|
+
spans = [m.span() for m in re.finditer(r"<!--.*?-->", line)]
|
|
7335
|
+
low = line.lower()
|
|
7336
|
+
found = []
|
|
7337
|
+
for key, pat, ctx in _CLAIM_PATTERNS:
|
|
7338
|
+
if ctx and ctx not in low:
|
|
7339
|
+
continue
|
|
7340
|
+
for m in re.finditer(pat, line, re.I):
|
|
7341
|
+
if any(a <= m.start() < b for a, b in spans):
|
|
7342
|
+
continue
|
|
7343
|
+
found.append((m.start(1), m.end(1), key, m.group(1)))
|
|
7344
|
+
out, taken = [], []
|
|
7345
|
+
for start, end, key, token in sorted(found):
|
|
7346
|
+
if any(start < e and s < end for s, e in taken):
|
|
7347
|
+
continue
|
|
7348
|
+
taken.append((start, end))
|
|
7349
|
+
out.append((key, start, end, token))
|
|
7350
|
+
return out
|
|
7351
|
+
|
|
7352
|
+
|
|
7353
|
+
def _write_claims(facts, paths):
|
|
7354
|
+
"""Rewrite every recognised STALE claim in `paths` to the derived fact, byte for byte
|
|
7355
|
+
otherwise. Line endings are preserved PER FILE (newline="" on both ends): the gated set mixes
|
|
7356
|
+
LF sources with CRLF-checked-out HTML, and normalising them would turn a one-token fix into a
|
|
7357
|
+
whole-file diff nobody can review.
|
|
7358
|
+
|
|
7359
|
+
What it deliberately does NOT touch: anything the patterns do not recognise. A missing
|
|
7360
|
+
subcommand table row, a count spelled outside 1..99, a claim phrased in prose -- those stay
|
|
7361
|
+
for the human, and the --check that follows still fails on them."""
|
|
7362
|
+
changed, total, problems = 0, 0, 0
|
|
7363
|
+
for path in paths:
|
|
7364
|
+
try:
|
|
7365
|
+
with open(path, encoding="utf-8", newline="") as fh:
|
|
7366
|
+
body = fh.read()
|
|
7367
|
+
except OSError as exc:
|
|
7368
|
+
print(" !! %s: unreadable: %s" % (path, exc))
|
|
7369
|
+
problems += 1
|
|
7370
|
+
continue
|
|
7371
|
+
except UnicodeDecodeError as exc:
|
|
7372
|
+
# `--check` reads this file with errors="replace" and still reports its claims, so
|
|
7373
|
+
# nothing is hidden. `--write` must NOT read it that way: writing a replaced byte
|
|
7374
|
+
# back would destroy data to fix a version number, and a writer that corrupts a file
|
|
7375
|
+
# to correct a claim is worse than the claim. Named, skipped, and the --check that
|
|
7376
|
+
# follows still fails on whatever is stale in it.
|
|
7377
|
+
print(" !! %s: not valid UTF-8 (%s) -- left untouched; --check still reads it"
|
|
7378
|
+
% (path, exc))
|
|
7379
|
+
problems += 1
|
|
7380
|
+
continue
|
|
7381
|
+
lines = body.split("\n")
|
|
7382
|
+
n = 0
|
|
7383
|
+
for i, line in enumerate(lines):
|
|
7384
|
+
claims = _iter_claims(line)
|
|
7385
|
+
if not claims:
|
|
7386
|
+
continue
|
|
7387
|
+
# right to left: an earlier rewrite must not move the offsets of a later one
|
|
7388
|
+
for key, start, end, token in sorted(claims, key=lambda c: c[1], reverse=True):
|
|
7389
|
+
actual = _fact_value(facts, key)
|
|
7390
|
+
if _claim_norm(token) == actual:
|
|
7391
|
+
continue
|
|
7392
|
+
line = line[:start] + _claim_rewrite(token, actual) + line[end:]
|
|
7393
|
+
n += 1
|
|
7394
|
+
lines[i] = line
|
|
7395
|
+
if not n:
|
|
7396
|
+
continue
|
|
7397
|
+
with open(path, "w", encoding="utf-8", newline="") as fh:
|
|
7398
|
+
fh.write("\n".join(lines))
|
|
7399
|
+
print("%s: %d claim(s) rewritten" % (path, n))
|
|
7400
|
+
changed += 1
|
|
7401
|
+
total += n
|
|
7402
|
+
print("FACTS --write: %d claim(s) rewritten in %d of %d file(s)%s"
|
|
7403
|
+
% (total, changed, len(paths),
|
|
7404
|
+
"" if not problems else "; %d file(s) could not be read" % problems))
|
|
7405
|
+
return problems
|
|
7406
|
+
|
|
7407
|
+
|
|
7271
7408
|
def cmd_facts(args):
|
|
7272
7409
|
"""Generate SYSTEM-FACTS.json, or --check published claims against the derived facts.
|
|
7273
7410
|
|
|
@@ -7276,6 +7413,24 @@ def cmd_facts(args):
|
|
|
7276
7413
|
about factual drift. A claim that CI does not compare against a derived fact will
|
|
7277
7414
|
drift; this makes the comparison mechanical and the drift a named red."""
|
|
7278
7415
|
facts = _derive_facts()
|
|
7416
|
+
if args.write is not None:
|
|
7417
|
+
# Until 1.97.0 there was no writer, so every bump was ~25 hand edits across ~13 files and
|
|
7418
|
+
# the release script could only refuse and hand them back. `--write` rewrites the claims
|
|
7419
|
+
# the SAME recogniser finds, then runs the SAME --check: a writer that reported its own
|
|
7420
|
+
# success would be exactly the self-graded evidence this engine exists to refuse.
|
|
7421
|
+
if args.check is not None:
|
|
7422
|
+
# Before 1.97.0 this combination silently dropped --check's files: --write set
|
|
7423
|
+
# args.check to ITS list. Two file sets given, one measured, no word about it.
|
|
7424
|
+
print("[qa_ledger] facts: --check and --write are alternatives, not a pair -- "
|
|
7425
|
+
"--write already re-checks exactly the files it wrote.", file=sys.stderr)
|
|
7426
|
+
sys.exit(2)
|
|
7427
|
+
if not args.write:
|
|
7428
|
+
print("[qa_ledger] facts: --write needs at least one file.", file=sys.stderr)
|
|
7429
|
+
sys.exit(2)
|
|
7430
|
+
if _write_claims(facts, args.write):
|
|
7431
|
+
# an unreadable file is an UNMEASURED claim set, and unmeasured is not green
|
|
7432
|
+
sys.exit(2)
|
|
7433
|
+
args.check = list(args.write)
|
|
7279
7434
|
if args.check:
|
|
7280
7435
|
problems = []
|
|
7281
7436
|
# 1) the committed facts file must match a fresh derivation (stale facts are drift)
|
|
@@ -7301,21 +7456,17 @@ def cmd_facts(args):
|
|
|
7301
7456
|
table_names = []
|
|
7302
7457
|
table_start = 0
|
|
7303
7458
|
for n, line in enumerate(lines, 1):
|
|
7304
|
-
#
|
|
7305
|
-
#
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
continue
|
|
7311
|
-
for m in re.finditer(pat, line, re.I):
|
|
7312
|
-
claimed = m.group(1)
|
|
7313
|
-
actual = _fact_value(facts, key)
|
|
7314
|
-
if claimed != actual:
|
|
7315
|
-
problems.append((path, n, key, claimed, actual))
|
|
7459
|
+
# the claims come from the SHARED recogniser (`_iter_claims`), which skips HTML
|
|
7460
|
+
# comment spans -- a comment is not a published claim
|
|
7461
|
+
for key, _s, _e, claimed in _iter_claims(line):
|
|
7462
|
+
actual = _fact_value(facts, key)
|
|
7463
|
+
if _claim_norm(claimed) != actual:
|
|
7464
|
+
problems.append((path, n, key, claimed, actual))
|
|
7316
7465
|
# the parser-surface table (Subcommand/Subcomando header, one `<td class="t">`
|
|
7317
7466
|
# row per subcommand) is a claim too, just not a numeric one -- a row can go
|
|
7318
7467
|
# missing while the count beside it stays correct (the `top` row did, once).
|
|
7468
|
+
# This half reads the comment-STRIPPED line: a commented-out row is not a row.
|
|
7469
|
+
line = re.sub(r"<!--.*?-->", "", line)
|
|
7319
7470
|
if not in_table:
|
|
7320
7471
|
if re.search(r"<th>Sub ?comm?ando?s?</th>", line, re.I):
|
|
7321
7472
|
in_table, table_names, table_start = True, [], n
|
|
@@ -12288,6 +12439,10 @@ def build_parser():
|
|
|
12288
12439
|
pfa.add_argument("--out", default="SYSTEM-FACTS.json")
|
|
12289
12440
|
pfa.add_argument("--check", nargs="*", default=None,
|
|
12290
12441
|
help="files whose claims must match the derived facts; exit 1 on drift")
|
|
12442
|
+
pfa.add_argument("--write", nargs="*", default=None,
|
|
12443
|
+
help="files whose recognised claims are REWRITTEN to the derived facts "
|
|
12444
|
+
"(spelled-out claims stay spelled out), then re-checked: exit 1 if "
|
|
12445
|
+
"anything still disagrees")
|
|
12291
12446
|
pfa.set_defaults(func=cmd_facts)
|
|
12292
12447
|
|
|
12293
12448
|
prt = sub.add_parser("roundtrip",
|
|
@@ -18,6 +18,7 @@ The human brings the idea, the constraints and the reference material. **You bri
|
|
|
18
18
|
shape.** Your job is to interrogate until there is a shared system shape, and to write
|
|
19
19
|
the documents as you go — not to ask the human to design the system for you.
|
|
20
20
|
|
|
21
|
+
<!-- uscha:orientation-block:begin -->
|
|
21
22
|
## First contact (show ONCE, then never again)
|
|
22
23
|
|
|
23
24
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -85,6 +86,7 @@ and say exactly what unblocks it.
|
|
|
85
86
|
|
|
86
87
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
87
88
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
89
|
+
<!-- uscha:orientation-block:end -->
|
|
88
90
|
|
|
89
91
|
## Non-negotiable principles
|
|
90
92
|
|
|
@@ -17,6 +17,7 @@ allowed-tools: Read, Write, Glob, Grep, Bash
|
|
|
17
17
|
Paints the REAL state of the project at a glance. It does not narrate or estimate: it
|
|
18
18
|
wires the JSON the engine emits into the template. Read-only.
|
|
19
19
|
|
|
20
|
+
<!-- uscha:orientation-block:begin -->
|
|
20
21
|
## Orientation markers (non-negotiable)
|
|
21
22
|
|
|
22
23
|
The operator must never have to ask "where am I?" or "what happens now?".
|
|
@@ -40,6 +41,7 @@ Run: <the exact command or skill to invoke>
|
|
|
40
41
|
including any `Flow:` line in this file. If nothing is actionable, say that plainly rather
|
|
41
42
|
than inventing a step. Keep the CONTENT in the conversation's language and the labels
|
|
42
43
|
(`Next`, `Run`) verbatim — the smoke suite checks for them.
|
|
44
|
+
<!-- uscha:orientation-block:end -->
|
|
43
45
|
|
|
44
46
|
## Contract
|
|
45
47
|
|
|
@@ -23,6 +23,7 @@ first, always — and what cannot be fact yet becomes an OBSERVATION in quaranti
|
|
|
23
23
|
evidence-classed, content-addressed, and promoted to the contract only by a per-observation
|
|
24
24
|
human verdict (ADR-013).**
|
|
25
25
|
|
|
26
|
+
<!-- uscha:orientation-block:begin -->
|
|
26
27
|
## First contact (show ONCE, then never again)
|
|
27
28
|
|
|
28
29
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -92,6 +93,7 @@ and say exactly what unblocks it.
|
|
|
92
93
|
|
|
93
94
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
94
95
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
96
|
+
<!-- uscha:orientation-block:end -->
|
|
95
97
|
|
|
96
98
|
## The one non-negotiable: quarantine, not judgment (ADR-009)
|
|
97
99
|
|
|
@@ -21,6 +21,7 @@ contract + `qa_ledger.py rubric-ingest` (stdlib, runs anywhere). ANY runner can
|
|
|
21
21
|
the grader — this skill just wraps the neutral prompt so Claude Code users get it
|
|
22
22
|
in one command. Never add Claude-specific behavior to the contract.
|
|
23
23
|
|
|
24
|
+
<!-- uscha:orientation-block:begin -->
|
|
24
25
|
## First contact (show ONCE, then never again)
|
|
25
26
|
|
|
26
27
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -88,6 +89,7 @@ and say exactly what unblocks it.
|
|
|
88
89
|
|
|
89
90
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
90
91
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
92
|
+
<!-- uscha:orientation-block:end -->
|
|
91
93
|
|
|
92
94
|
## Protocol
|
|
93
95
|
|
|
@@ -19,6 +19,7 @@ skill (**pull** — one screen when the human asks), and the **mirador** (bird's
|
|
|
19
19
|
HTML). This skill exists because some surfaces never show a statusline; the answer
|
|
20
20
|
is the same data, printed in chat when requested.
|
|
21
21
|
|
|
22
|
+
<!-- uscha:orientation-block:begin -->
|
|
22
23
|
## Orientation markers (non-negotiable)
|
|
23
24
|
|
|
24
25
|
The operator must never have to ask "where am I?" or "what happens now?".
|
|
@@ -42,6 +43,7 @@ Run: <the exact command or skill to invoke>
|
|
|
42
43
|
including any `Flow:` line in this file. If nothing is actionable, say that plainly rather
|
|
43
44
|
than inventing a step. Keep the CONTENT in the conversation's language and the labels
|
|
44
45
|
(`Next`, `Run`) verbatim — the smoke suite checks for them.
|
|
46
|
+
<!-- uscha:orientation-block:end -->
|
|
45
47
|
|
|
46
48
|
## Contract
|
|
47
49
|
|
|
@@ -21,6 +21,7 @@ switch between at any time:
|
|
|
21
21
|
- **Technical track** — architecture, modules, data flow, contracts, QA results,
|
|
22
22
|
coverage, known deferred issues.
|
|
23
23
|
|
|
24
|
+
<!-- uscha:orientation-block:begin -->
|
|
24
25
|
## First contact (show ONCE, then never again)
|
|
25
26
|
|
|
26
27
|
**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
|
|
@@ -88,6 +89,7 @@ and say exactly what unblocks it.
|
|
|
88
89
|
|
|
89
90
|
Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
|
|
90
91
|
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
|
|
92
|
+
<!-- uscha:orientation-block:end -->
|
|
91
93
|
|
|
92
94
|
## Inputs
|
|
93
95
|
|