dw-sdlc 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  // Reads what a repository declares for an agent: the committed config, the machine's local layer over it,
2
- // and what the repository implies; an invalid layer is dropped whole. The guardrails plugin's
2
+ // and what the repository implies; an invalid layer is dropped whole. The sdlc plugin's
3
3
  // `hooks/config.jq` is the same merge for the guards; this file is the reference, and the tests run both.
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { existsSync, readFileSync } from "node:fs";
@@ -40,9 +40,11 @@ export type Inputs = {
40
40
  };
41
41
  /** Claude Code's config dir: `$CLAUDE_CONFIG_DIR` when set, the way Claude Code reads it, else `~/.claude`. */
42
42
  export declare function claudeConfigDir(env: Record<string, string | undefined>, home: string): string;
43
- /** The marketplace Claude Code registers this repository under, and the plugin id inside it. */
43
+ /** The marketplace Claude Code registers this repository under, the plugin in it, and the package the plugin links to. */
44
44
  export declare const MARKETPLACE = "dw-sdlc";
45
- export declare const PLUGIN_ID = "guardrails@dw-sdlc";
45
+ export declare const PLUGIN = "sdlc";
46
+ export declare const PLUGIN_ID = "sdlc@dw-sdlc";
47
+ export declare const PACKAGE = "dw-sdlc";
46
48
  /** rtk find passes -exec through verbatim from this release; before it, `rtk find … -exec` exits 1. */
47
49
  export declare const RTK_FIND_EXEC_FIXED = "0.46.0";
48
50
  export declare const PNPM_MINIMUM_RELEASE_AGE = 10080;
@@ -53,5 +55,5 @@ export declare function rtkConfigPath(home: string, platform?: string, env?: Rec
53
55
  export declare function rtkExcludes(toml: string | undefined): string[];
54
56
  export declare function diagnose(inputs: Inputs): Check[];
55
57
  export declare function render(checks: Check[]): string;
56
- export declare const summary = "one PASS / WARN / FAIL line per machine check the guardrails rely on";
58
+ export declare const summary = "one PASS / WARN / FAIL line per machine check the sdlc plugin relies on";
57
59
  export declare function run(argv: string[]): number;
@@ -1,19 +1,22 @@
1
- // `dw-sdlc doctor`: one PASS / WARN / FAIL line per machine check the guardrails rely on, for the
1
+ // `dw-sdlc doctor`: one PASS / WARN / FAIL line per machine check the sdlc plugin relies on, for the
2
2
  // repository holding --root and the user's home; it installs nothing. FAIL is a guard that does not guard
3
- // (no jq, plugin absent or disabled, an invalid config), WARN a layer missing or loose, INFO one that does not apply.
3
+ // (no jq, plugin absent or disabled, an invalid config), WARN a layer missing, loose or out of step, INFO one that does not apply.
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { parseArgs } from "node:util";
9
9
  import { findRepository, resolveConfig } from "../agentic-config.js";
10
+ import { EXACT_VERSION, parseCatalog, parsePins } from "../pins.js";
10
11
  /** Claude Code's config dir: `$CLAUDE_CONFIG_DIR` when set, the way Claude Code reads it, else `~/.claude`. */
11
12
  export function claudeConfigDir(env, home) {
12
13
  return env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
13
14
  }
14
- /** The marketplace Claude Code registers this repository under, and the plugin id inside it. */
15
+ /** The marketplace Claude Code registers this repository under, the plugin in it, and the package the plugin links to. */
15
16
  export const MARKETPLACE = "dw-sdlc";
16
- export const PLUGIN_ID = `guardrails@${MARKETPLACE}`;
17
+ export const PLUGIN = "sdlc";
18
+ export const PLUGIN_ID = `${PLUGIN}@${MARKETPLACE}`;
19
+ export const PACKAGE = "dw-sdlc";
17
20
  /** rtk find passes -exec through verbatim from this release; before it, `rtk find … -exec` exits 1. */
18
21
  export const RTK_FIND_EXEC_FIXED = "0.46.0";
19
22
  export const PNPM_MINIMUM_RELEASE_AGE = 10080;
@@ -64,7 +67,7 @@ function liveCheckout(probe, claude) {
64
67
  function pluginOrigin(probe, claude, installPath) {
65
68
  const checkout = liveCheckout(probe, claude);
66
69
  if (checkout !== undefined) {
67
- return `runs live from ${checkout}/plugins/guardrails; a change there is in the next session`;
70
+ return `runs live from ${checkout}/plugins/${PLUGIN}; a change there is in the next session`;
68
71
  }
69
72
  return `installed at ${installPath}; a change reaches it through claude plugin update`;
70
73
  }
@@ -72,7 +75,7 @@ function pluginOrigin(probe, claude, installPath) {
72
75
  function runningPluginRoot(probe, claude) {
73
76
  const checkout = liveCheckout(probe, claude);
74
77
  if (checkout !== undefined) {
75
- return join(checkout, "plugins", "guardrails");
78
+ return join(checkout, "plugins", PLUGIN);
76
79
  }
77
80
  const { status, stdout } = probe.run("claude", ["plugin", "list", "--json"]);
78
81
  const list = status === 0 ? parseJson(stdout) : undefined;
@@ -110,6 +113,57 @@ function checkPlugin(probe, claude) {
110
113
  detail: `${version} enabled; ${pluginOrigin(probe, claude, String(entry.installPath ?? ""))}`
111
114
  };
112
115
  }
116
+ /** The `version` of the package.json in `dir`, or undefined when there is none. */
117
+ function manifestVersion(probe, dir) {
118
+ const manifest = parseJson(probe.read(join(dir, "package.json")));
119
+ return isObject(manifest) && typeof manifest.version === "string" ? manifest.version : undefined;
120
+ }
121
+ /** Whether the plugin's hooks and skills run the dw-sdlc the repository pins, the one its Gate runs. */
122
+ function checkPackage(probe, claude, root) {
123
+ const name = "package";
124
+ const manifest = root === undefined ? undefined : probe.read(join(root, "package.json"));
125
+ if (root === undefined || manifest === undefined) {
126
+ return { name, level: "INFO", detail: `no package.json here to pin ${PACKAGE}` };
127
+ }
128
+ let spec;
129
+ try {
130
+ spec = parsePins(manifest, parseCatalog(probe.read(join(root, "pnpm-workspace.yaml")) ?? ""))[PACKAGE];
131
+ }
132
+ catch {
133
+ return { name, level: "INFO", detail: `${join(root, "package.json")} is not JSON` };
134
+ }
135
+ if (spec === undefined) {
136
+ return { name, level: "INFO", detail: `the repository does not pin ${PACKAGE}` };
137
+ }
138
+ // `workspace:` or a range names no one version: the copy installed under node_modules is the one that runs.
139
+ const pinned = EXACT_VERSION.test(spec) ? spec : manifestVersion(probe, join(root, "node_modules", PACKAGE));
140
+ if (pinned === undefined) {
141
+ return { name, level: "WARN", detail: `the repository pins ${PACKAGE} ${spec}, not installed: pnpm install` };
142
+ }
143
+ const plugin = runningPluginRoot(probe, claude);
144
+ if (plugin === undefined) {
145
+ return { name, level: "INFO", detail: `no ${PLUGIN_ID} runs here to compare with the pinned ${pinned}` };
146
+ }
147
+ const linked = manifestVersion(probe, join(plugin, PACKAGE));
148
+ if (linked === undefined) {
149
+ return {
150
+ name,
151
+ level: "WARN",
152
+ detail: `the plugin at ${plugin} carries no ${PACKAGE}: an install from GitHub needs packages in its --sparse set`
153
+ };
154
+ }
155
+ if (linked !== pinned) {
156
+ const remedy = versionBefore(linked, pinned)
157
+ ? `claude plugin update ${PLUGIN_ID}`
158
+ : `pnpm add --save-dev --save-exact ${PACKAGE}@${linked}`;
159
+ return {
160
+ name,
161
+ level: "WARN",
162
+ detail: `drift: the plugin runs ${PACKAGE} ${linked} and the repository pins ${pinned}, so its hooks and skills run other code than the Gate. ${remedy}`
163
+ };
164
+ }
165
+ return { name, level: "PASS", detail: `${PACKAGE} ${linked} in the plugin and in the repository` };
166
+ }
113
167
  /** Every hook command string in a settings file, whatever the event and matcher. */
114
168
  function hookCommands(settings) {
115
169
  if (!isObject(settings) || !isObject(settings.hooks)) {
@@ -286,6 +340,7 @@ export function diagnose(inputs) {
286
340
  return [
287
341
  checkJq(probe),
288
342
  checkPlugin(probe, claude),
343
+ checkPackage(probe, claude, inputs.config === undefined ? undefined : inputs.root),
289
344
  checkRtk(probe, home, claude),
290
345
  checkSafeChain(probe),
291
346
  checkPnpmReleaseAge(probe, home),
@@ -296,11 +351,11 @@ export function diagnose(inputs) {
296
351
  export function render(checks) {
297
352
  return checks.map((check) => `${check.level.padEnd(5)} ${check.name.padEnd(17)} ${check.detail}`).join("\n");
298
353
  }
299
- export const summary = "one PASS / WARN / FAIL line per machine check the guardrails rely on";
354
+ export const summary = "one PASS / WARN / FAIL line per machine check the sdlc plugin relies on";
300
355
  const USAGE = `Usage: dw-sdlc doctor [--root <dir>] [--home <dir>]
301
356
  or: pnpm guardrails:doctor [--root <dir>] [--home <dir>], in a checkout of dw-sdlc
302
357
 
303
- One PASS / WARN / FAIL line per machine check the guardrails rely on; read-only. Exit 1 when any
358
+ One PASS / WARN / FAIL line per machine check the sdlc plugin relies on; read-only. Exit 1 when any
304
359
  check fails. Claude Code's settings are read from $CLAUDE_CONFIG_DIR, or ~/.claude without it; a
305
360
  --home reads <home>/.claude.`;
306
361
  function repositoryConfig(root) {
@@ -25,7 +25,7 @@ const IGNORED = "references/*";
25
25
  const TRACKED = ["!references/AGENTS.md", "!references/CLAUDE.md"];
26
26
  /** Lines that ignore references/ itself: git never looks inside it, so no exception tracks the Router. */
27
27
  const WHOLE = new Set(["references", "references/", "/references", "/references/"]);
28
- const IGNORE_COMMENT = "# References are cloned on each machine by the references plugin; only the Router is tracked.";
28
+ const IGNORE_COMMENT = "# References are cloned on each machine by the sdlc plugin; only the Router is tracked.";
29
29
  export const summary = "set a repository up to declare References: the Router, its table and the ignore rules";
30
30
  const USAGE = `Usage: dw-sdlc references setup [--root <dir>] [--dry-run | --check]
31
31
  or: pnpm references:setup with the same flags, in a checkout of dw-sdlc
@@ -1,4 +1,4 @@
1
- // `dw-sdlc setup` sets a repository and this machine up for the guardrails plugin: the four files its USAGE
1
+ // `dw-sdlc setup` sets a repository and this machine up for the sdlc plugin: the four files its USAGE
2
2
  // names, each written only when its content would change, so a run is safe to repeat. Existing content is
3
3
  // merged, never replaced: other keys stay, and a settings file that is not JSON is refused.
4
4
  import { spawnSync } from "node:child_process";
@@ -35,7 +35,7 @@ function isCommitOnProtected(value) {
35
35
  }
36
36
  /** Where Claude Code registers the plugin: the marketplace name and the plugin's name inside it. */
37
37
  const MARKETPLACE = "dw-sdlc";
38
- const PLUGIN = "guardrails";
38
+ const PLUGIN = "sdlc";
39
39
  /** The plugin's link to the dw-sdlc package, which carries the schema. */
40
40
  const PACKAGE_LINK = "dw-sdlc";
41
41
  function isObject(value) {
package/dist/pins.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /** Versions by package name: what package.json pins, or what a catalog lists. */
2
2
  export type Pins = Record<string, string>;
3
+ /** A pin that names one version: `x.y.z`, a pre-release tag allowed. */
4
+ export declare const EXACT_VERSION: RegExp;
3
5
  /**
4
6
  * The default catalog in pnpm-workspace.yaml's text: each `name: version` line of its top-level
5
7
  * `catalog:` block, a comment after the key allowed. Without the block, it is empty.
package/dist/pins.js CHANGED
@@ -3,7 +3,8 @@
3
3
  // ref without `{name}` reads neither file, so a broken manifest fails only the References that name one.
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
- const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/;
6
+ /** A pin that names one version: `x.y.z`, a pre-release tag allowed. */
7
+ export const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/;
7
8
  /** The two ways a pin names the default catalog: by its name, or by pnpm's shorthand. */
8
9
  const DEFAULT_CATALOG = new Set(["catalog:", "catalog:default"]);
9
10
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dw-sdlc",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Gate and the agentic config of a repository: the dw-sdlc bin with the Gate, the setup, the doctor, the config check, and the Sync and setup of References; the resolver of the config's layers, its JSON Schema and the validator that reads it.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@ No Reference is declared yet: `/sdlc-references-add <repository>` adds one.
28
28
  `AGENTS.md`. It describes _that_ repository; `claudeMdExcludes` in `.claude/settings.json` keeps
29
29
  Claude Code from loading them.
30
30
  - **Missing is normal.** CI, a fresh clone and a new worktree have none of them until a local Claude
31
- Code session starts with the references plugin enabled, which syncs them. A directory that is
31
+ Code session starts with the sdlc plugin enabled, which syncs them. A directory that is
32
32
  absent is not a broken link, and neither is a path its entry's `skip` leaves out.
33
33
  - **A starting point, not the whole field.** The list is hand-picked, and each clone is its ref at
34
34
  the last sync. Research into a feature or an approach reads the matching entries first, then looks