@skitterbyte/skitterspec 16.5.0 → 16.6.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/assets/rules/negative-checks.md +69 -0
- package/package.json +1 -1
- package/src/init.js +20 -5
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Checks That Accuse
|
|
2
|
+
|
|
3
|
+
A check **accuses** when being wrong costs something: it deletes, it exits
|
|
4
|
+
non-zero, or it tells the user their code is broken. Those checks earn the four
|
|
5
|
+
rules below. An ordinary conditional does not — this is about the ones that act
|
|
6
|
+
on what they conclude.
|
|
7
|
+
|
|
8
|
+
Almost every accusation begins as an **absence**: a name not in a list, a
|
|
9
|
+
directory not on disk, a version not in a response. An absence is evidence only
|
|
10
|
+
once you have established that the lookup could have seen the thing. Three
|
|
11
|
+
times in one day, in unrelated code, we established nothing and acted anyway:
|
|
12
|
+
|
|
13
|
+
| Absence observed | Concluded | What had blinded the lookup |
|
|
14
|
+
|------------------|-----------|-----------------------------|
|
|
15
|
+
| ref not in the issue list | "it does not exist" | the query excluded archived issues, and capped at 250 |
|
|
16
|
+
| version not in the registry's list | "the publish failed" | the registry is eventually consistent |
|
|
17
|
+
| lifecycle folder not on disk | "half-installed" | git does not store an empty directory |
|
|
18
|
+
|
|
19
|
+
The bills: 146 healthy refs accused, a valid release tag deleted, a non-zero
|
|
20
|
+
exit on a healthy repo. Each blind spot was knowable in advance.
|
|
21
|
+
|
|
22
|
+
## 1. Prefer a positive signal to an absence
|
|
23
|
+
|
|
24
|
+
Assert something that must be **present**, not something that must not be
|
|
25
|
+
missing. A positive signal fails loudly when you are wrong about it; an absence
|
|
26
|
+
fails silently whenever the lookup was narrower than you assumed.
|
|
27
|
+
|
|
28
|
+
The scaffold check above stopped asking "is the lifecycle folder there?" — a
|
|
29
|
+
folder git drops as soon as it empties — and started asking whether the config
|
|
30
|
+
folder the installer always writes into is there. Same intent, and the new
|
|
31
|
+
question has an answer.
|
|
32
|
+
|
|
33
|
+
Where no positive signal exists, widen the lookup until absence means
|
|
34
|
+
something (include the archived rows, ask the API for the one id rather than
|
|
35
|
+
scanning a page) — or do not conclude.
|
|
36
|
+
|
|
37
|
+
## 2. Name the blind spot beside the check
|
|
38
|
+
|
|
39
|
+
A comment naming what could make this lookup lie is what makes the next reader
|
|
40
|
+
check it. Not what the code does — what would fool it:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// A LIFECYCLE FOLDER IS NOT CHECKED, deliberately. git does not track empty
|
|
44
|
+
// directories, so it disappears whenever the bucket empties and returns the
|
|
45
|
+
// moment something lands in it.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Write it when you write the check, while you still know why it is safe.
|
|
49
|
+
|
|
50
|
+
## 3. Pair every accusation with a stays-silent test
|
|
51
|
+
|
|
52
|
+
For each accusing check, a test that feeds it a **healthy but unusual** input
|
|
53
|
+
and asserts it says nothing: the empty bucket, the archived record, the
|
|
54
|
+
just-published version, the file the user edited on purpose. The positive test
|
|
55
|
+
proves the check can fire; only this one proves it does not fire at everyone
|
|
56
|
+
else. All three incidents would have been caught by it, and prose alone had
|
|
57
|
+
already failed to prevent them.
|
|
58
|
+
|
|
59
|
+
## 4. Bias the unknown case toward inaction
|
|
60
|
+
|
|
61
|
+
Three states, not two: yes, no, and *cannot tell*. Route the third to the
|
|
62
|
+
harmless branch — skip, warn, keep, retry — never to the destructive one.
|
|
63
|
+
|
|
64
|
+
The install manifest classifies a file whose hash it does not recognise as
|
|
65
|
+
`customized` rather than stale, so a resync **keeps** it (`managedState`,
|
|
66
|
+
`packages/common/src/init.js`). An unrecognised hash could mean a user's edit or
|
|
67
|
+
a lost manifest; only one of those readings is safe to act on, so it takes that
|
|
68
|
+
one. Being wrong there costs a redundant file on disk. The opposite default
|
|
69
|
+
costs the user their work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.6.0",
|
|
4
4
|
"description": "Spec-driven development for Claude Code — a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/init.js
CHANGED
|
@@ -406,6 +406,11 @@ function installClaudeMd(dir, { mode }) {
|
|
|
406
406
|
|
|
407
407
|
// True when the repo looks already set up: any managed file present, any spec
|
|
408
408
|
// lifecycle folder, or the CLAUDE.md spec marker (Decision 1 — detect eagerly).
|
|
409
|
+
// Is skitterspec already installed here? Matched on what we ACTUALLY install —
|
|
410
|
+
// our managed files, our lifecycle folders, our CLAUDE.md marker — never on
|
|
411
|
+
// `.claude/` merely existing: someone else's skills are not evidence of ours,
|
|
412
|
+
// and reading them as ours would treat every Claude Code project as a
|
|
413
|
+
// half-finished install.
|
|
409
414
|
function isExistingSetup(dir) {
|
|
410
415
|
if (managedTargets(dir).some((t) => fs.existsSync(t.abs))) return true
|
|
411
416
|
if (SPEC_FOLDERS.some((f) => fs.existsSync(path.join(dir, 'specs', f)))) return true
|
|
@@ -562,11 +567,21 @@ function printReport(dir, mode, { diff = false } = {}) {
|
|
|
562
567
|
// ships none. Discovering it from what was actually installed keeps this file
|
|
563
568
|
// tracker-free — it never has to know which tracker (if any) is in the box.
|
|
564
569
|
const setupSkill = SKILLS.find((s) => /^spec-.+-setup$/.test(s))
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
+
const provider = setupSkill ? /^spec-(.+)-setup$/.exec(setupSkill)[1] : null
|
|
571
|
+
// …and the same derivation gives the provider's config filename, so this can
|
|
572
|
+
// report tracker sync the way it reports isolation above — from what is
|
|
573
|
+
// actually on disk. It used to say "opt-in: run /…-setup" even on a repo that
|
|
574
|
+
// had already configured it, telling you to set up what was already set up.
|
|
575
|
+
const trackerOn =
|
|
576
|
+
provider && fs.existsSync(path.join(dir, 'specs', '.core', `${provider}.config.json`))
|
|
577
|
+
const trackerNote = !setupSkill
|
|
578
|
+
? ''
|
|
579
|
+
: trackerOn
|
|
580
|
+
? `Tracker sync is ON: ${provider} — the repo stays the source of truth;` +
|
|
581
|
+
' /spec-push mirrors a spec up and /spec-status reports drift.\n'
|
|
582
|
+
: `Tracker sync is opt-in: run /${setupSkill} to configure it` +
|
|
583
|
+
' (it discovers your workspace and writes the config), or see' +
|
|
584
|
+
' specs/.core/SETUP.md.\n'
|
|
570
585
|
process.stdout.write(
|
|
571
586
|
'\nDone. Skills resolve as /spec, /spec-go, /spec-complete, /spec-cancel,' +
|
|
572
587
|
' /spec-bug, /spec-review, /spec-init, /spec-connect.\n' +
|