@windyroad/wardley 0.2.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "wr-wardley",
3
+ "version": "0.1.0",
4
+ "description": "Wardley Map generation for Claude Code"
5
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const utils = await import(resolve(__dirname, "../../shared/install-utils.mjs"));
8
+
9
+ const PLUGIN = "wr-wardley";
10
+ const DEPS = [];
11
+
12
+ const flags = utils.parseStandardArgs(process.argv);
13
+
14
+ if (flags.help) {
15
+ console.log(`
16
+ Usage: npx @windyroad/wardley [options]
17
+
18
+ Wardley Map generation
19
+
20
+ Options:
21
+ --update Update this plugin and its skills
22
+ --uninstall Remove this plugin
23
+ --dry-run Show what would be done without executing
24
+ --help, -h Show this help
25
+ `);
26
+ process.exit(0);
27
+ }
28
+
29
+ if (flags.dryRun) {
30
+ utils.setDryRun(true);
31
+ console.log("[dry-run mode — no commands will be executed]\n");
32
+ }
33
+
34
+ utils.checkPrerequisites();
35
+
36
+ if (flags.uninstall) {
37
+ utils.uninstallPackage(PLUGIN);
38
+ } else if (flags.update) {
39
+ utils.updatePackage(PLUGIN);
40
+ } else {
41
+ utils.installPackage(PLUGIN, { deps: DEPS });
42
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@windyroad/wardley",
3
+ "version": "0.2.0",
4
+ "description": "Wardley Map generation",
5
+ "bin": {
6
+ "windyroad-wardley": "./bin/install.mjs"
7
+ },
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/windyroad/agent-plugins.git",
13
+ "directory": "packages/wardley"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "claude-code-plugin",
18
+ "ai-agent",
19
+ "ai-coding"
20
+ ],
21
+ "files": [
22
+ "bin/",
23
+ "agents/",
24
+ "hooks/",
25
+ "skills/",
26
+ ".claude-plugin/"
27
+ ]
28
+ }
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: wr:wardley
3
+ description: Generate or update the project's Wardley Map by analyzing the codebase. Produces an OWM source file, SVG, and PNG.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Wardley Map Generator
8
+
9
+ Analyze the codebase and generate a Wardley Map of the project's value chain.
10
+
11
+ ## Output files
12
+
13
+ - `docs/wardley-map.owm` (OWM source)
14
+ - `docs/wardley-map.svg` (rendered SVG)
15
+ - `docs/wardley-map.png` (rendered PNG)
16
+ - `docs/wardley-map.md` (analysis with embedded map image)
17
+
18
+ ## Steps
19
+
20
+ ### 1. Discover the user need
21
+
22
+ Read the project's README, homepage, main entry point, or package description to determine what need the project serves. The anchor must be a specific need, not a person. "Software delivery insight" not "Reader". "Real-time cricket stats" not "User". The anchor should distinguish this project from others.
23
+
24
+ **Self-test:** If two different projects could share this anchor, it is too generic. The anchor should answer "what does someone come to this project to get?" not "what topic does this project cover?"
25
+
26
+ **Need vs means:** The anchor should be an outcome the user *has* after they leave, not a capability the provider *offers*. If the anchor sounds like a service description (expertise, guidance, consulting), rewrite it as the result: "AI Delivery Expertise" becomes "Reliable AI-Built Software." "Analytics consulting" becomes "Data-driven decisions."
27
+
28
+ ### 2. Inventory the codebase
29
+
30
+ Scan the project to identify components. Look for:
31
+
32
+ - **User-facing outputs**: UI, API endpoints, CLI commands, reports, pages
33
+ - **Content or data**: authored content, databases, data pipelines, models
34
+ - **Processing logic**: custom business logic, transformation pipelines, plugins
35
+ - **Quality enforcement**: testing, linting, review gates, compliance checks
36
+ - **Infrastructure**: hosting, CI/CD, build tools, deployment, monitoring
37
+ - **Dependencies**: frameworks, libraries, external services
38
+
39
+ Adapt the scan to the project type. A web app has pages and components. An API has endpoints and middleware. A CLI has commands and parsers. A library has modules and public API surface.
40
+
41
+ **Platform dependencies:** For each custom or genesis component, identify which platform or API it depends on that the project does not control. If that platform does not appear in the inventory, add it as a component.
42
+
43
+ ### 3. Classify evolution
44
+
45
+ For each component, determine its evolution stage:
46
+
47
+ | Stage | Evolution (x) | Characteristics |
48
+ |-------|--------------|-----------------|
49
+ | Genesis | 0.00 to 0.17 | Novel, no off-the-shelf equivalent, high uncertainty |
50
+ | Custom-Built | 0.17 to 0.37 | Understood but bespoke, built for this project |
51
+ | Product | 0.37 to 0.63 | Standardised, configurable, multiple providers exist |
52
+ | Commodity | 0.63 to 1.00 | Utility, interchangeable, barely noticed when working |
53
+
54
+ ### 4. Position on value chain
55
+
56
+ Visibility (y-axis): 1.0 = directly serves the user need, 0.0 = deep infrastructure the user never thinks about.
57
+
58
+ **Visibility reality check:** For each component, ask: "Does the user directly interact with this or see its output?" If yes (login screens, dashboards, error messages), visibility should be above 0.5 even if the component feels like infrastructure. Auth systems the user sees should not be positioned like databases the user never sees.
59
+
60
+ ### 5. Decide what to split and merge
61
+
62
+ Aim for 8 to 12 components. Rules:
63
+
64
+ - **Split** when two things have different evolution positions (one custom, one commodity) or different strategic roles (one differentiates, one is plumbing).
65
+ - **Merge** when two things are at the same evolution stage and serve the same strategic purpose. **Test:** can you invest in A independently of B? If not, they are one component.
66
+ - Every component should earn its place. If removing it from the map loses no information, remove it.
67
+
68
+ **Hidden-but-critical check:** Look for components that are invisible to the user but gate or constrain visible components. CI/CD pipelines, compliance gates, and release processes may have low visibility but high strategic impact. If such a component can block a release or degrade quality, consider whether it needs a dependency link to the component it constrains — not just to the infrastructure it uses.
69
+
70
+ ### 6. Identify evolution movement
71
+
72
+ For each component, ask: is this evolving? Signs of evolution:
73
+
74
+ - You are writing articles about it (moving from Genesis toward Custom-Built)
75
+ - Multiple projects now use the same pattern (moving toward Product)
76
+ - An off-the-shelf alternative has emerged (moving toward Commodity)
77
+ - You are considering replacing a custom solution with a standard one
78
+
79
+ Add `evolve` annotations for components that are actively moving.
80
+
81
+ **Inertia check:** For commodity components, note switching cost (high or low). For custom components, note whether competitors or the ecosystem are building similar things. Use these observations in the Risk section of the analysis.
82
+
83
+ ### 7. Map dependencies
84
+
85
+ A->B means "A needs B to function." Not "A influences B" or "A produces B." Only draw links that represent real runtime or build-time dependencies. Fewer lines with clear meaning beats many lines that add noise.
86
+
87
+ **User-facing pair check:** For every pair of components with visibility above 0.7, ask: does one need the other to get traffic, data, or users? If articles drive traffic that service pages convert, that is a dependency even though both connect to the anchor.
88
+
89
+ ### 8. Generate the OWM file
90
+
91
+ Write `docs/wardley-map.owm` using OWM syntax:
92
+
93
+ ```
94
+ title Project Value Chain
95
+
96
+ anchor User Need [0.95, 0.55]
97
+
98
+ component Name [visibility, evolution]
99
+
100
+ evolve Component Name 0.45
101
+
102
+ From->To
103
+ ```
104
+
105
+ ### 9. Render
106
+
107
+ ```bash
108
+ node ${CLAUDE_SKILL_DIR}/owm-to-svg.mjs
109
+ ```
110
+
111
+ ### 10. Verify
112
+
113
+ Read the generated PNG and check:
114
+
115
+ - No overlapping labels
116
+ - Dependencies flow logically
117
+ - Evolution positions match the phase boundaries
118
+ - Evolution arrows show meaningful movement
119
+ - The map tells a coherent story: where is the differentiation? What is evolving? What is commodity?
120
+
121
+ If the map has issues, adjust positions in the OWM file and re-render.
122
+
123
+ **Orphan check:** Look for components with only one inbound link and no outbound links. For each orphan, choose one:
124
+ 1. Add a missing dependency that justifies it (e.g., a gate component should link to what it gates, not just what it reads from).
125
+ 2. Remove it if it adds no strategic information.
126
+ Do not leave orphans unexplained — they confuse readers into thinking the component is disconnected from the value chain.
127
+
128
+ **Domain context check:** Can a reader unfamiliar with the domain understand *why* the anchor is hard to serve? If the challenge is non-obvious (real-time constraints, hostile environments, regulatory pressure), add a one-line OWM comment (`// context: ...`) above the anchor explaining the constraint. This comment will not render but documents intent for future updates.
129
+
130
+ ### 11. Write the analysis
131
+
132
+ Write `docs/wardley-map.md` with the following structure:
133
+
134
+ ```markdown
135
+ # Wardley Map
136
+
137
+ ![Wardley Map](wardley-map.png)
138
+
139
+ ## Analysis
140
+
141
+ ### Differentiation
142
+ (Which components are custom/genesis? These are the competitive advantage. If multiple components depend on the same custom component, note that convergence is also concentration risk. State what would happen if that component became untenable.)
143
+
144
+ ### Evolution
145
+ (What is moving? In which direction? What does that mean for investment? Check whether any evolving component has dependents that amplify its impact. If A depends on B and B is evolving, investment in B has a multiplier effect. Name the multiplier.)
146
+
147
+ ### Risk
148
+ (Which custom components could be replaced by commodities? Which commodities could change?)
149
+
150
+ ### Decisions
151
+ (Does the map surface any strategic choices the project should make?)
152
+ ```
153
+
154
+ The image path should be relative to `docs/` since the PNG lives in the same directory. Keep the analysis concise and actionable. Each section should be 2 to 4 sentences.
155
+
156
+ **Analysis quality rules:**
157
+
158
+ - **No inventory.** Do not list component counts, script counts, or file counts. State what the position *means* for investment, risk, or action.
159
+ - **Name the phase correctly.** When describing a component's current position, use the phase name matching its current evolution value, not its evolve target. Check against the phase boundary table.
160
+ - **Risk needs triggers.** For each risk, name what would cause it (the trigger) and what breaks (the consequence). "Low risk" is not analysis.
161
+ - **At most two decisions.** The Decisions section should identify at most two strategic choices and state them as trade-offs. If two actions compete for the same resource, say so. Do not write a shopping list.
162
+ - **State evidence for recommendations.** If you recommend an action (e.g. "ready to be packaged"), say what evidence supports it and what would need to be true first.
163
+ - **End with implications.** Every section must end with a sentence stating what the project owner should do differently, protect, or watch as a result. Description without implication is not analysis.
164
+ - **Include internal risk.** The Risk section must include at least one risk the project owner controls, such as underinvestment in a differentiating component. External risks (API changes, pricing shifts) are not sufficient alone. Internal risk triggers must include a number and a time window. "Output drops" is not a trigger. "Fewer than N per month for M consecutive months" is.
165
+ - **Observable triggers.** Each decision's trigger must be observable and specific: a number, an event, or a condition you could check. "When external interest validates" is not observable. "When three people outside this project ask to reuse the pattern" is. Each decision should have both a go trigger (what would make you act) and a reassess trigger (what would make you reconsider the strategy if the go trigger never fires).
166
+ - **Cover visible dependencies.** Every dependency between components with visibility above 0.7 must be mentioned in the analysis. If a link appears on the map but not in the text, either remove the link or explain why it matters.
167
+ - **Cover all custom components in Differentiation.** Every component in the custom-built or genesis phase must appear in the Differentiation section. Do not highlight some custom components while silently omitting others. If a custom component has fragile internals (non-standard APIs, complex state, platform-specific workarounds), name that fragility.
168
+ - **Cover commodity risk.** The Risk section must include at least one commodity-layer risk (pricing changes, service discontinuation, API deprecation). State the trigger, the affected components, and the fallback. Do not treat commodities as zero-risk simply because they are interchangeable today.
169
+ - **Triggers must be measurable today.** Each trigger must be something you can check with tools or data the project already has. If the trigger requires building new infrastructure to measure (a test suite that does not exist, a monitoring system not yet deployed), say so explicitly and name the prerequisite. A trigger you cannot currently observe is a hypothesis, not a trigger.
170
+ - **No project-specific names in decisions.** Decision triggers and reassess triggers must be stated generically. Do not name specific external projects, people, or organisations. Use descriptions like "a second project" or "an external adopter" instead of proper nouns. The analysis should remain valid if read by someone outside the immediate team.
171
+
172
+ ## Updating an existing map
173
+
174
+ If `docs/wardley-map.owm` already exists, read it first. Compare against the current codebase:
175
+
176
+ - Add components for things that have been added to the project
177
+ - Remove components for things that no longer exist
178
+ - Adjust evolution positions for things that have matured
179
+ - Add or update `evolve` arrows for things in motion
180
+ - Preserve the user's positioning choices where the codebase has not changed
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Converts an OWM (Online Wardley Mapping) file to SVG and PNG.
4
+ *
5
+ * Usage: node owm-to-svg.mjs [input.owm] [output.svg]
6
+ *
7
+ * Defaults:
8
+ * input: docs/wardley-map.owm
9
+ * output: docs/wardley-map.svg (+ .png via sips)
10
+ */
11
+
12
+ import { readFileSync, writeFileSync } from 'fs';
13
+ import { execSync } from 'child_process';
14
+ import { resolve } from 'path';
15
+
16
+ const inputPath = resolve(process.argv[2] || 'docs/wardley-map.owm');
17
+ const outputSvg = resolve(process.argv[3] || inputPath.replace(/\.owm$/, '.svg'));
18
+ const outputPng = outputSvg.replace(/\.svg$/, '.png');
19
+
20
+ const raw = readFileSync(inputPath, 'utf8');
21
+ const lines = raw.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('//'));
22
+
23
+ // Parse
24
+ let title = '';
25
+ const components = [];
26
+ const links = [];
27
+ const evolves = [];
28
+
29
+ for (const line of lines) {
30
+ const titleMatch = line.match(/^title\s+(.+)/);
31
+ if (titleMatch) { title = titleMatch[1]; continue; }
32
+
33
+ const anchorMatch = line.match(/^anchor\s+(.+?)\s+\[([0-9.]+),\s*([0-9.]+)\]/);
34
+ if (anchorMatch) {
35
+ components.push({ name: anchorMatch[1], vis: parseFloat(anchorMatch[2]), evo: parseFloat(anchorMatch[3]), isAnchor: true });
36
+ continue;
37
+ }
38
+
39
+ const compMatch = line.match(/^component\s+(.+?)\s+\[([0-9.]+),\s*([0-9.]+)\]/);
40
+ if (compMatch) {
41
+ components.push({ name: compMatch[1], vis: parseFloat(compMatch[2]), evo: parseFloat(compMatch[3]), isAnchor: false });
42
+ continue;
43
+ }
44
+
45
+ const evolveMatch = line.match(/^evolve\s+(.+?)\s+([0-9.]+)/);
46
+ if (evolveMatch) {
47
+ evolves.push({ name: evolveMatch[1], targetEvo: parseFloat(evolveMatch[2]) });
48
+ continue;
49
+ }
50
+
51
+ const linkMatch = line.match(/^(.+?)->(.+)/);
52
+ if (linkMatch) {
53
+ links.push({ from: linkMatch[1].trim(), to: linkMatch[2].trim() });
54
+ continue;
55
+ }
56
+ }
57
+
58
+ // Layout constants
59
+ const W = 1200;
60
+ const H = 800;
61
+ const PAD_LEFT = 80;
62
+ const PAD_RIGHT = 60;
63
+ const PAD_TOP = 60;
64
+ const PAD_BOTTOM = 80;
65
+ const CHART_W = W - PAD_LEFT - PAD_RIGHT;
66
+ const CHART_H = H - PAD_TOP - PAD_BOTTOM;
67
+
68
+ function evoToX(evo) { return PAD_LEFT + evo * CHART_W; }
69
+ function visToY(vis) { return PAD_TOP + (1 - vis) * CHART_H; }
70
+
71
+ // Build SVG
72
+ const svgParts = [];
73
+ svgParts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">`);
74
+ svgParts.push(`<defs>
75
+ <marker id="evolve-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
76
+ <polygon points="0,0 8,3 0,6" fill="#c44"/>
77
+ </marker>
78
+ </defs>`);
79
+ svgParts.push(`<style>
80
+ text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
81
+ .title { font-size: 20px; font-weight: 700; }
82
+ .axis-label { font-size: 12px; fill: #666; }
83
+ .phase-label { font-size: 11px; fill: #999; }
84
+ .comp-label { font-size: 13px; fill: #222; }
85
+ .anchor-label { font-size: 13px; fill: #222; font-weight: 600; }
86
+ .evolve-label { font-size: 11px; fill: #c44; }
87
+ </style>`);
88
+
89
+ // Background
90
+ svgParts.push(`<rect width="${W}" height="${H}" fill="white"/>`);
91
+
92
+ // Title
93
+ svgParts.push(`<text x="${PAD_LEFT}" y="35" class="title">${title}</text>`);
94
+
95
+ // Axes
96
+ svgParts.push(`<line x1="${PAD_LEFT}" y1="${PAD_TOP}" x2="${PAD_LEFT}" y2="${H - PAD_BOTTOM}" stroke="#333" stroke-width="1.5"/>`);
97
+ svgParts.push(`<line x1="${PAD_LEFT}" y1="${H - PAD_BOTTOM}" x2="${W - PAD_RIGHT}" y2="${H - PAD_BOTTOM}" stroke="#333" stroke-width="1.5"/>`);
98
+
99
+ // Arrow on evolution axis
100
+ const arrowX = W - PAD_RIGHT;
101
+ const arrowY = H - PAD_BOTTOM;
102
+ svgParts.push(`<polygon points="${arrowX},${arrowY} ${arrowX - 8},${arrowY - 4} ${arrowX - 8},${arrowY + 4}" fill="#333"/>`);
103
+
104
+ // Axis titles
105
+ svgParts.push(`<text x="${PAD_LEFT - 10}" y="${PAD_TOP + CHART_H / 2}" class="axis-label" transform="rotate(-90 ${PAD_LEFT - 10} ${PAD_TOP + CHART_H / 2})" text-anchor="middle">Value Chain</text>`);
106
+ svgParts.push(`<text x="${PAD_LEFT + CHART_W / 2}" y="${H - 15}" class="axis-label" text-anchor="middle">Evolution</text>`);
107
+
108
+ // Phase dividers and labels
109
+ const phases = [
110
+ { boundary: 0.17, label: 'Genesis' },
111
+ { boundary: 0.37, label: 'Custom-Built' },
112
+ { boundary: 0.63, label: 'Product' },
113
+ { boundary: 1.0, label: 'Commodity' },
114
+ ];
115
+
116
+ let prevBound = 0;
117
+ for (const phase of phases) {
118
+ const midEvo = (prevBound + phase.boundary) / 2;
119
+ svgParts.push(`<text x="${evoToX(midEvo)}" y="${H - PAD_BOTTOM + 25}" class="phase-label" text-anchor="middle">${phase.label}</text>`);
120
+ if (phase.boundary < 1.0) {
121
+ const dx = evoToX(phase.boundary);
122
+ svgParts.push(`<line x1="${dx}" y1="${PAD_TOP}" x2="${dx}" y2="${H - PAD_BOTTOM}" stroke="#ddd" stroke-width="1" stroke-dasharray="4,4"/>`);
123
+ }
124
+ prevBound = phase.boundary;
125
+ }
126
+
127
+ // Links
128
+ const compMap = new Map(components.map((c) => [c.name, c]));
129
+ for (const link of links) {
130
+ const from = compMap.get(link.from);
131
+ const to = compMap.get(link.to);
132
+ if (!from || !to) continue;
133
+ svgParts.push(`<line x1="${evoToX(from.evo)}" y1="${visToY(from.vis)}" x2="${evoToX(to.evo)}" y2="${visToY(to.vis)}" stroke="#aaa" stroke-width="1.5"/>`);
134
+ }
135
+
136
+ // Evolution arrows
137
+ for (const ev of evolves) {
138
+ const comp = compMap.get(ev.name);
139
+ if (!comp) continue;
140
+ const x1 = evoToX(comp.evo);
141
+ const x2 = evoToX(ev.targetEvo);
142
+ const y = visToY(comp.vis);
143
+ svgParts.push(`<line x1="${x1 + 8}" y1="${y}" x2="${x2 - 2}" y2="${y}" stroke="#c44" stroke-width="2" stroke-dasharray="6,3" marker-end="url(#evolve-arrow)"/>`);
144
+ svgParts.push(`<circle cx="${x2}" cy="${y}" r="5" fill="none" stroke="#c44" stroke-width="1.5" stroke-dasharray="3,2"/>`);
145
+ }
146
+
147
+ // Components
148
+ for (const comp of components) {
149
+ const cx = evoToX(comp.evo);
150
+ const cy = visToY(comp.vis);
151
+ const r = comp.isAnchor ? 0 : 6;
152
+ const labelClass = comp.isAnchor ? 'anchor-label' : 'comp-label';
153
+
154
+ if (!comp.isAnchor) {
155
+ svgParts.push(`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" stroke="#333" stroke-width="1.5"/>`);
156
+ }
157
+ svgParts.push(`<text x="${cx + 10}" y="${cy - 10}" class="${labelClass}">${comp.name}</text>`);
158
+ }
159
+
160
+ svgParts.push('</svg>');
161
+
162
+ const svg = svgParts.join('\n');
163
+ writeFileSync(outputSvg, svg);
164
+ console.log(`SVG: ${outputSvg}`);
165
+
166
+ // Convert to PNG via sips (macOS)
167
+ try {
168
+ execSync(`sips -s format png "${outputSvg}" --out "${outputPng}" 2>/dev/null`);
169
+ console.log(`PNG: ${outputPng}`);
170
+ } catch {
171
+ console.log('PNG: skipped (sips not available, macOS only)');
172
+ }