@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 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.95.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
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.95.0",
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"(\d+)\s+sub-?comm?ands", None),
7289
+ ("subcommands.count", _COUNT + r"sub-?comm?ands", None),
7259
7290
  ("subcommands.count", r"(\d+)\s+subcomandos", None),
7260
- ("skills.count", r"(\d+)\s+skills", None),
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
- # an HTML comment is not a published claim -- the first live run flagged a
7305
- # section marker (a comment reading "2 Skills") as a drifted count
7306
- line = re.sub(r"<!--.*?-->", "", line)
7307
- low = line.lower()
7308
- for key, pat, ctx in _CLAIM_PATTERNS:
7309
- if ctx and ctx not in low:
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.95.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.95.0",
3
+ "version": "1.97.0",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.95.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
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.95.0
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"(\d+)\s+sub-?comm?ands", None),
7289
+ ("subcommands.count", _COUNT + r"sub-?comm?ands", None),
7259
7290
  ("subcommands.count", r"(\d+)\s+subcomandos", None),
7260
- ("skills.count", r"(\d+)\s+skills", None),
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
- # an HTML comment is not a published claim -- the first live run flagged a
7305
- # section marker (a comment reading "2 Skills") as a drifted count
7306
- line = re.sub(r"<!--.*?-->", "", line)
7307
- low = line.lower()
7308
- for key, pat, ctx in _CLAIM_PATTERNS:
7309
- if ctx and ctx not in low:
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,5 +1,5 @@
1
1
  {
2
- "version": "1.95.0",
2
+ "version": "1.97.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,