@stdd/cli 0.0.1 → 0.0.2

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
@@ -132,10 +132,14 @@ $ npx @stdd/cli doctor
132
132
  Then wire the guards into CI:
133
133
 
134
134
  ```yaml
135
+ - uses: actions/checkout@v4
136
+ with:
137
+ fetch-depth: 0 # check-pr --base needs history
135
138
  - run: npx @stdd/cli check # tree invariants
136
139
  - env:
137
140
  PR_BODY: ${{ github.event.pull_request.body }}
138
- run: printf '%s' "$PR_BODY" | npx @stdd/cli check-pr - # docs evidence line
141
+ # Evidence line present AND the claimed doc paths really changed in this PR
142
+ run: printf '%s' "$PR_BODY" | npx @stdd/cli check-pr - --base "origin/${{ github.base_ref }}"
139
143
  ```
140
144
 
141
145
  ## Commands
@@ -145,7 +149,7 @@ Then wire the guards into CI:
145
149
  | `stdd init [dir] [--tools claude,codex]` | Install `.stdd/` and compile playbooks per agent |
146
150
  | `stdd doctor [dir]` | Adoption health report: setup, canonical docs, misleading artifacts, drift — exits 1 on findings |
147
151
  | `stdd check [dir]` | CI guard: no committed working artifacts, no temporal narrative in canonical docs, no stale or hand-edited generated files |
148
- | `stdd check-pr <file\|->` | CI guard: PR body carries exactly one non-empty docs evidence line |
152
+ | `stdd check-pr <file\|-> [--base <ref>]` | CI guard: PR body carries exactly one non-empty docs evidence line; with `--base`, claimed doc paths are verified against the actual git diff |
149
153
 
150
154
  All checks are configured in `.stdd/config.json` (glob patterns for
151
155
  forbidden artifacts, canonical docs, temporal phrases).
@@ -173,7 +177,8 @@ forbidden artifacts, canonical docs, temporal phrases).
173
177
  history → git; deferred designs → dated project-log entries.
174
178
  5. **Evidence, not claims.** Every PR states `Docs updated first:` /
175
179
  `Docs checked, no change needed:` / `Docs not applicable:` — naming the
176
- docs or the reason. CI rejects a missing, duplicated, or bare label.
180
+ docs or the reason. CI rejects a missing, duplicated, or bare label, and
181
+ with `--base` verifies the claimed doc paths against the actual diff.
177
182
 
178
183
  The full contract: [`method/README.md`](method/README.md).
179
184
 
package/cli/lib.mjs CHANGED
@@ -92,6 +92,14 @@ export function mergeConfig(parsed) {
92
92
  return config;
93
93
  }
94
94
 
95
+ /**
96
+ * Extract repo-relative markdown paths from an evidence line's content.
97
+ * Prose (reasons, dashes, backticks) around the paths is ignored.
98
+ */
99
+ export function extractDocPaths(content) {
100
+ return content.match(/[A-Za-z0-9_][A-Za-z0-9_./-]*\.md\b/g) ?? [];
101
+ }
102
+
95
103
  /**
96
104
  * Build a temporal-phrase matcher. Word-ish boundaries on both sides so
97
105
  * hyphenated compounds ("no longer-lived") do not match.
package/cli/stdd.mjs CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import {
7
7
  DEFAULT_CONFIG,
8
+ extractDocPaths,
8
9
  findEvidenceLines,
9
10
  globToRegExp,
10
11
  mergeConfig,
@@ -295,7 +296,7 @@ function doctor(targetDir) {
295
296
  if (failed) process.exit(1);
296
297
  }
297
298
 
298
- function checkPr(prBodyFile) {
299
+ function checkPr(prBodyFile, baseRef) {
299
300
  const body =
300
301
  prBodyFile === "-" || !prBodyFile ? fs.readFileSync(0, "utf8") : fs.readFileSync(prBodyFile, "utf8");
301
302
  const matches = findEvidenceLines(body);
@@ -313,16 +314,52 @@ function checkPr(prBodyFile) {
313
314
  if (matches[0].content === "") {
314
315
  fail(`"${matches[0].label}:" names no evidence — list the docs or the reason after the colon.`);
315
316
  }
317
+ if (baseRef) verifyEvidenceAgainstDiff(matches[0], baseRef);
316
318
  console.log("stdd check-pr: OK");
317
319
  }
318
320
 
321
+ /** With --base: the evidence claim must be backed by the actual git diff. */
322
+ function verifyEvidenceAgainstDiff({ label, content }, baseRef) {
323
+ const paths = extractDocPaths(content);
324
+ if (label === "Docs updated first") {
325
+ if (paths.length === 0) {
326
+ fail(`"Docs updated first:" names no doc paths — list the changed docs.`);
327
+ }
328
+ let changed;
329
+ try {
330
+ changed = execFileSync("git", ["diff", "--name-only", `${baseRef}...HEAD`], {
331
+ encoding: "utf8",
332
+ stdio: ["ignore", "pipe", "pipe"],
333
+ })
334
+ .split("\n")
335
+ .filter(Boolean);
336
+ } catch (err) {
337
+ fail(`--base ${baseRef}: git diff failed: ${err.stderr?.toString().trim() || err.message}`);
338
+ }
339
+ const missing = paths.filter((p) => !changed.includes(p));
340
+ if (missing.length > 0) {
341
+ fail(`claimed as updated but not changed against ${baseRef}: ${missing.join(", ")}`);
342
+ }
343
+ } else if (label === "Docs checked, no change needed") {
344
+ const absent = paths.filter((p) => !fs.existsSync(path.join(process.cwd(), ...p.split("/"))));
345
+ if (absent.length > 0) {
346
+ fail(`claimed as checked but does not exist in the tree: ${absent.join(", ")}`);
347
+ }
348
+ }
349
+ }
350
+
319
351
  // --- argument parsing (strict: unknown flags are errors) ---
320
352
  const [, , command, ...rest] = process.argv;
321
353
  let tools = null;
354
+ let baseRefArg = null;
322
355
  const positional = [];
323
356
  for (let i = 0; i < rest.length; i++) {
324
357
  const arg = rest[i];
325
- if (arg === "--tools" || arg.startsWith("--tools=")) {
358
+ if (arg === "--base" || arg.startsWith("--base=")) {
359
+ if (command !== "check-pr") fail(`--base is only valid for "stdd check-pr"`);
360
+ baseRefArg = arg.includes("=") ? arg.slice("--base=".length) : (rest[++i] ?? "");
361
+ if (!baseRefArg) fail("--base requires a git ref, e.g. --base origin/main");
362
+ } else if (arg === "--tools" || arg.startsWith("--tools=")) {
326
363
  if (command !== "init") fail(`--tools is only valid for "stdd init"`);
327
364
  const value = arg.includes("=") ? arg.slice("--tools=".length) : (rest[++i] ?? "");
328
365
  tools = value.split(",").filter(Boolean);
@@ -351,13 +388,15 @@ switch (command) {
351
388
  doctor(targetDir);
352
389
  break;
353
390
  case "check-pr":
354
- checkPr(positional[0]);
391
+ checkPr(positional[0], baseRefArg);
355
392
  break;
356
393
  case "--version":
357
394
  case "version":
358
395
  console.log(VERSION);
359
396
  break;
360
397
  default:
361
- console.log("Usage: stdd <init|check|check-pr|doctor> [dir|pr-body-file] [--tools claude,codex]");
398
+ console.log(
399
+ "Usage: stdd <init|check|check-pr|doctor> [dir|pr-body-file] [--tools claude,codex] [--base <ref>]",
400
+ );
362
401
  process.exit(command ? 1 : 0);
363
402
  }
package/method/README.md CHANGED
@@ -55,6 +55,12 @@ classify → read docs → docs edit (the spec) → failing test → implement
55
55
  starting at the beginning of a line counts (quoted templates and code
56
56
  blocks do not).
57
57
 
58
+ With `--base <ref>` the claim is verified against the actual diff:
59
+ every doc path named after `Docs updated first:` must be a file changed
60
+ between the base ref and `HEAD` (and at least one path must be named);
61
+ paths named after `Docs checked, no change needed:` must exist in the
62
+ tree. Claiming a docs update the diff does not contain fails CI.
63
+
58
64
  ## The frontend exception: design-first
59
65
 
60
66
  Frontend **visual** work — layout, styling, markup structure, presentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stdd/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Spec + Test Driven Development — a markdown-first methodology kit for teams building software with AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {