inclusion-md 0.1.0 → 0.2.1
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 +72 -1
- package/bin/inclusion-md.js +33 -9
- package/lib/ascii.js +60 -0
- package/lib/init.js +253 -110
- package/lib/prompt.js +37 -12
- package/lib/questionnaire.js +285 -0
- package/lib/template.js +36 -11
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -94,11 +94,51 @@ npx inclusion-md init --variant design-system # start from a domain example
|
|
|
94
94
|
npx inclusion-md init --out docs/INCLUSION.md # write somewhere else
|
|
95
95
|
npx inclusion-md init --yes # accept defaults, non-interactive
|
|
96
96
|
npx inclusion-md init --force # overwrite without prompting
|
|
97
|
+
npx inclusion-md update # re-run questionnaire on existing file
|
|
97
98
|
npx inclusion-md --help
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
Requires Node.js 16+. No dependencies.
|
|
101
102
|
|
|
103
|
+
#### The Design Decisions questionnaire
|
|
104
|
+
|
|
105
|
+
After the foundational questions, the CLI offers an opt-in **Design Decisions**
|
|
106
|
+
questionnaire - 13 short, skippable questions across six groups:
|
|
107
|
+
|
|
108
|
+
1. **Core assumptions** - who you primarily build for, and what "default user"
|
|
109
|
+
assumptions live inside your product
|
|
110
|
+
2. **Authentication & access** - how people get in, who can't, why
|
|
111
|
+
3. **Information collection** - what you ask for, what's required, why
|
|
112
|
+
4. **Interaction model** - how people interact, what patterns you don't support
|
|
113
|
+
5. **Communication & language** - languages and tone you optimize for
|
|
114
|
+
6. **Edge cases & intersections** - where the product breaks, who shows up
|
|
115
|
+
in unexpected ways
|
|
116
|
+
|
|
117
|
+
The questions are grounded in three sources:
|
|
118
|
+
|
|
119
|
+
- _ABLEIST: Measuring Ableist Harms in LLMs_ ([arxiv.org/abs/2510.10998](https://arxiv.org/abs/2510.10998))
|
|
120
|
+
- _Centering Disability Perspectives in LLM Research and Design_ (ACM)
|
|
121
|
+
- Kat Holmes, _Mismatch: How Inclusion Shapes Design_ (MIT Press)
|
|
122
|
+
|
|
123
|
+
The point isn't perfect answers. The point is **conscious, documented
|
|
124
|
+
tradeoffs**. Skipping a question is a valid answer; the doc just won't speak
|
|
125
|
+
to that dimension yet. Run `npx inclusion-md update` later to fill more in.
|
|
126
|
+
|
|
127
|
+
Answers land in your `INCLUSION.md` as a `### 1.B Design Decisions` subsection
|
|
128
|
+
under Project Context. Reviewers (human and AI) can flag generated output that
|
|
129
|
+
contradicts the realities you've documented.
|
|
130
|
+
|
|
131
|
+
#### Updating an existing INCLUSION.md
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx inclusion-md update
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This re-runs the Project Context + Design Decisions + Maintenance questions
|
|
138
|
+
and rewrites **only** Sections 1 and 12 in place. Any edits you've made to
|
|
139
|
+
the rest of the file - your engineering guidance, your language heuristics,
|
|
140
|
+
your custom sections - are preserved.
|
|
141
|
+
|
|
102
142
|
### Option B: Copy the file by hand
|
|
103
143
|
|
|
104
144
|
```bash
|
|
@@ -152,7 +192,7 @@ finalizing any generated output in this repository.
|
|
|
152
192
|
**Continue / Windsurf / Cody / etc.** - add `INCLUSION.md` to your workspace
|
|
153
193
|
context configuration.
|
|
154
194
|
|
|
155
|
-
###
|
|
195
|
+
### Treat it like the rest of your engineering docs
|
|
156
196
|
|
|
157
197
|
- Name an owner.
|
|
158
198
|
- Review on a cadence (quarterly recommended).
|
|
@@ -193,6 +233,37 @@ disabled people - with authority, budget, and time.
|
|
|
193
233
|
|
|
194
234
|
---
|
|
195
235
|
|
|
236
|
+
## Troubleshooting
|
|
237
|
+
|
|
238
|
+
**"`npx inclusion-md` does nothing / hangs."**
|
|
239
|
+
Make sure you're on Node.js 16+ (`node --version`). The CLI uses only
|
|
240
|
+
built-in modules - no install step is required.
|
|
241
|
+
|
|
242
|
+
**"I can't find an existing `INCLUSION.md`" when running `update`.**
|
|
243
|
+
By default the CLI looks at `./INCLUSION.md`. If yours lives elsewhere,
|
|
244
|
+
pass `--out`:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npx inclusion-md update --out docs/INCLUSION.md
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**The welcome animation looks weird in CI.**
|
|
251
|
+
It's automatically skipped when stdout isn't a TTY, when `--no-color` is
|
|
252
|
+
passed, or when `CI=1` is set. If you're piping output, pass `--no-color`
|
|
253
|
+
explicitly to be safe.
|
|
254
|
+
|
|
255
|
+
**I want to script this in a setup wizard.**
|
|
256
|
+
Pass `--yes` to accept all defaults and skip the optional Design Decisions
|
|
257
|
+
questionnaire. Combine with `--variant`, `--out`, and `--force` for a fully
|
|
258
|
+
non-interactive run.
|
|
259
|
+
|
|
260
|
+
**I edited my `INCLUSION.md` and I'm worried `update` will trash it.**
|
|
261
|
+
`update` only touches Section 1 (Project Context) and Section 12
|
|
262
|
+
(Maintenance). Everything else is preserved verbatim. The smoke tests
|
|
263
|
+
([`test/smoke.test.js`](./test/smoke.test.js)) cover this contract.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
196
267
|
## Contributing
|
|
197
268
|
|
|
198
269
|
Pull requests, issues, translations, domain-specific extensions, and critiques
|
package/bin/inclusion-md.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
|
|
4
|
-
const { init } = require("../lib/init");
|
|
4
|
+
const { init, update } = require("../lib/init");
|
|
5
5
|
|
|
6
|
-
const HELP = `inclusion-md - scaffold an INCLUSION.md for your repository.
|
|
6
|
+
const HELP = `inclusion-md - scaffold and maintain an INCLUSION.md for your repository.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
npx inclusion-md
|
|
9
|
+
npx inclusion-md <command> [options]
|
|
10
10
|
npx inclusion-md --help
|
|
11
11
|
|
|
12
12
|
Commands:
|
|
13
13
|
init Interactively generate an INCLUSION.md.
|
|
14
|
+
Includes an opt-in 13-question Design Decisions
|
|
15
|
+
questionnaire that surfaces the real tradeoffs
|
|
16
|
+
your product has made.
|
|
17
|
+
update Re-run the questionnaire against an existing
|
|
18
|
+
INCLUSION.md. Rewrites Section 1 (Project Context)
|
|
19
|
+
and Section 12 (Maintenance) in place. Everything
|
|
20
|
+
else - including your edits - is preserved.
|
|
14
21
|
|
|
15
22
|
Options:
|
|
16
23
|
-o, --out <path> Output path (default: ./INCLUSION.md).
|
|
17
|
-
--variant <name> Start from a variant template:
|
|
24
|
+
--variant <name> Start from a variant template (init only):
|
|
18
25
|
generic (default) | frontend-app | design-system | backend-api
|
|
19
26
|
--force Overwrite an existing INCLUSION.md without prompting.
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
-y, --yes Accept defaults for any unanswered prompts.
|
|
28
|
+
Skips the optional Design Decisions questionnaire.
|
|
29
|
+
--no-color Disable ANSI color output and skip the welcome animation.
|
|
22
30
|
-h, --help Show this help.
|
|
23
31
|
-v, --version Show CLI version.
|
|
24
32
|
|
|
@@ -26,8 +34,18 @@ Examples:
|
|
|
26
34
|
npx inclusion-md init
|
|
27
35
|
npx inclusion-md init --variant design-system
|
|
28
36
|
npx inclusion-md init --out docs/INCLUSION.md --force
|
|
37
|
+
npx inclusion-md update
|
|
38
|
+
npx inclusion-md update --out docs/INCLUSION.md
|
|
39
|
+
|
|
40
|
+
Troubleshooting:
|
|
41
|
+
- "I can't find an existing INCLUSION.md" - run \`init\` first, or pass
|
|
42
|
+
--out to point at the right path.
|
|
43
|
+
- Animation looks off in CI - it's automatically skipped when stdout is
|
|
44
|
+
not a TTY, when --no-color is passed, or when CI=1 is set.
|
|
45
|
+
- Want to script this? Use --yes to skip prompts and accept defaults.
|
|
29
46
|
|
|
30
47
|
Read more: https://github.com/BranonConor/inclusion.md
|
|
48
|
+
Companion essay: https://branon.dev/blog/posts/the-need-for-inclusion-md
|
|
31
49
|
`;
|
|
32
50
|
|
|
33
51
|
function parseArgs(argv) {
|
|
@@ -74,14 +92,16 @@ async function main() {
|
|
|
74
92
|
}
|
|
75
93
|
if (!args.command) args.command = "init";
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
const COMMANDS = { init, update };
|
|
96
|
+
const handler = COMMANDS[args.command];
|
|
97
|
+
if (!handler) {
|
|
78
98
|
console.error(`Unknown command: ${args.command}`);
|
|
79
99
|
process.stdout.write("\n" + HELP);
|
|
80
100
|
process.exit(2);
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
try {
|
|
84
|
-
await
|
|
104
|
+
await handler(args);
|
|
85
105
|
} catch (err) {
|
|
86
106
|
if (err && err.code === "USER_CANCELLED") {
|
|
87
107
|
console.error("\nCancelled. No file was written.");
|
|
@@ -92,4 +112,8 @@ async function main() {
|
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
main
|
|
115
|
+
if (require.main === module) {
|
|
116
|
+
main();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { parseArgs, HELP };
|
package/lib/ascii.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const c = require("./colors");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gentle 3-frame welcome animation. Conveys "coming together" via dots that
|
|
7
|
+
* gradually connect. ASCII-only, no flashing, no inverted colors, no Unicode.
|
|
8
|
+
*
|
|
9
|
+
* Accessibility:
|
|
10
|
+
* - Skipped automatically when stdout is not a TTY (CI, piped output).
|
|
11
|
+
* - Skipped when --no-color is passed (color is the cue, not motion).
|
|
12
|
+
* - Skipped when --yes is passed (non-interactive runs).
|
|
13
|
+
* - Frames change appearance gradually (dim -> normal -> bold), not flashing.
|
|
14
|
+
* - Total runtime under 1 second.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const FRAMES = [
|
|
18
|
+
[" . . . ", " ", " . . . "],
|
|
19
|
+
[" o o o ", " ", " o o o "],
|
|
20
|
+
[" o-----o-----o ", " | ", " o-----o-----o "],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function renderFrame(frame, intensity) {
|
|
24
|
+
// intensity: 0 = dim, 1 = normal, 2 = bold
|
|
25
|
+
const stylize =
|
|
26
|
+
intensity === 0 ? c.dim : intensity === 2 ? c.bold : (s) => s;
|
|
27
|
+
return frame.map((line) => stylize(line)).join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearFrame(lineCount) {
|
|
31
|
+
for (let i = 0; i < lineCount; i++) {
|
|
32
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function play({ enabled = true, frameMs = 220 } = {}) {
|
|
41
|
+
if (!enabled) return;
|
|
42
|
+
for (let i = 0; i < FRAMES.length; i++) {
|
|
43
|
+
const intensity = i === FRAMES.length - 1 ? 2 : i;
|
|
44
|
+
process.stdout.write(renderFrame(FRAMES[i], intensity) + "\n");
|
|
45
|
+
if (i < FRAMES.length - 1) {
|
|
46
|
+
await sleep(frameMs);
|
|
47
|
+
clearFrame(FRAMES[i].length + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldAnimate({ noColor, yes }) {
|
|
53
|
+
if (yes) return false;
|
|
54
|
+
if (noColor) return false;
|
|
55
|
+
if (!process.stdout.isTTY) return false;
|
|
56
|
+
if (process.env.CI) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { play, shouldAnimate, FRAMES };
|
package/lib/init.js
CHANGED
|
@@ -6,33 +6,142 @@ const path = require("path");
|
|
|
6
6
|
const c = require("./colors");
|
|
7
7
|
const { createPrompter } = require("./prompt");
|
|
8
8
|
const { loadTemplate, renderGeneric, VARIANTS } = require("./template");
|
|
9
|
+
const ascii = require("./ascii");
|
|
10
|
+
const { runQuestionnaire, TOTAL_QUESTIONS } = require("./questionnaire");
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
${c.dim("An LLM/agent context convention for model biases - https://github.com/BranonConor/inclusion.md")}
|
|
13
|
-
`;
|
|
12
|
+
const REPO_URL = "https://github.com/BranonConor/inclusion.md";
|
|
13
|
+
const ESSAY_URL = "https://branon.dev/blog/posts/the-need-for-inclusion-md";
|
|
14
14
|
|
|
15
|
-
async function
|
|
15
|
+
async function welcome(args) {
|
|
16
16
|
c.set(args.color);
|
|
17
|
+
await ascii.play({
|
|
18
|
+
enabled: ascii.shouldAnimate({ noColor: !args.color, yes: args.yes }),
|
|
19
|
+
});
|
|
20
|
+
process.stdout.write(
|
|
21
|
+
"\n" +
|
|
22
|
+
c.magenta(c.bold("Welcome to the INCLUSION.md CLI!")) +
|
|
23
|
+
" " +
|
|
24
|
+
c.dim("(v" + require("../package.json").version + ")") +
|
|
25
|
+
"\n\n",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function askProjectContext(p) {
|
|
30
|
+
process.stdout.write(
|
|
31
|
+
"\n" +
|
|
32
|
+
c.bold("Project Context") +
|
|
33
|
+
"\n" +
|
|
34
|
+
c.dim(
|
|
35
|
+
"These fill in Section 1. Be specific - generic answers make for generic guidance.\n",
|
|
36
|
+
) +
|
|
37
|
+
"\n",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const product = await p.text("What does this project do?", {
|
|
41
|
+
default: "A short description of what this software does.",
|
|
42
|
+
});
|
|
43
|
+
const primaryUsers = await p.text(
|
|
44
|
+
"Who have you intentionally designed for?",
|
|
45
|
+
{ default: "Describe your primary users." },
|
|
46
|
+
);
|
|
47
|
+
const underservedUsers = await p.text(
|
|
48
|
+
"Who do you know you haven't designed for well yet?",
|
|
49
|
+
{
|
|
50
|
+
default:
|
|
51
|
+
"Document a real gap (e.g. screen reader users on the dashboard; Spanish-language onboarding).",
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
const distribution = await p.text(
|
|
55
|
+
"Where is this software used? (geography, devices, network, assistive tech)",
|
|
56
|
+
{
|
|
57
|
+
default:
|
|
58
|
+
"Web/iOS/Android, primary regions, languages, common assistive technologies.",
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
const stakes = await p.text(
|
|
62
|
+
"What happens when this software excludes someone?",
|
|
63
|
+
{
|
|
64
|
+
default:
|
|
65
|
+
"Describe the real-world cost (loss of access to healthcare, employment, civic services).",
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return { product, primaryUsers, underservedUsers, distribution, stakes };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function askMaintenance(p) {
|
|
73
|
+
process.stdout.write("\n" + c.bold("Maintenance") + "\n\n");
|
|
74
|
+
const owner = await p.text("Who owns this file? (person or team)", {
|
|
75
|
+
default: "An accountable person or team",
|
|
76
|
+
});
|
|
77
|
+
const cadence = await p.choice(
|
|
78
|
+
"How often will this file be reviewed?",
|
|
79
|
+
[
|
|
80
|
+
{ value: "Monthly.", label: "Monthly" },
|
|
81
|
+
{ value: "Quarterly.", label: "Quarterly", hint: "Recommended." },
|
|
82
|
+
{ value: "Twice a year.", label: "Twice a year" },
|
|
83
|
+
{ value: "Annually (minimum).", label: "Annually (minimum)" },
|
|
84
|
+
],
|
|
85
|
+
{ default: 1 },
|
|
86
|
+
);
|
|
87
|
+
const feedback = await p.text(
|
|
88
|
+
"How can users and contributors report exclusionary patterns?",
|
|
89
|
+
{
|
|
90
|
+
default:
|
|
91
|
+
"Open an issue on this repository, or contact the owner directly.",
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
return { owner, cadence, feedback };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function init(args) {
|
|
98
|
+
await welcome(args);
|
|
17
99
|
|
|
18
|
-
process.stdout.write(BANNER + "\n");
|
|
19
100
|
process.stdout.write(
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
101
|
+
"Let's generate an inclusion context document for your project. There are\n" +
|
|
102
|
+
"two simple parts to this:\n\n" +
|
|
103
|
+
` ${c.cyan("Part 1")} I'll start from a foundational template with general\n` +
|
|
104
|
+
` inclusion guidance to help AI agents reason about their\n` +
|
|
105
|
+
` training-data biases.\n\n` +
|
|
106
|
+
` ${c.cyan("Part 2")} Your turn. A short Project Context questionnaire (and an\n` +
|
|
107
|
+
` optional, deeper Design Decisions one) so I can fold your\n` +
|
|
108
|
+
` project-specific reality into the doc.\n\n` +
|
|
109
|
+
"Then we're done.\n\n",
|
|
24
110
|
);
|
|
25
111
|
|
|
26
112
|
const p = createPrompter({ acceptDefaults: !!args.yes });
|
|
27
113
|
|
|
28
114
|
try {
|
|
115
|
+
const proceed = await p.confirm("Should we dive in?", { default: true });
|
|
116
|
+
if (!proceed) {
|
|
117
|
+
process.stdout.write(
|
|
118
|
+
"\n" +
|
|
119
|
+
c.cyan(
|
|
120
|
+
"No worries! Come back whenever you're ready. Your future users will thank you.",
|
|
121
|
+
) +
|
|
122
|
+
"\n",
|
|
123
|
+
);
|
|
124
|
+
p.close();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(
|
|
129
|
+
"\n" +
|
|
130
|
+
c.green("✓ ") +
|
|
131
|
+
"Awesome. Step 1: loading the foundational template + general inclusion guidance...\n",
|
|
132
|
+
);
|
|
133
|
+
|
|
29
134
|
// --- variant ----------------------------------------------------------
|
|
30
135
|
let variant = args.variant;
|
|
31
136
|
if (!variant) {
|
|
32
137
|
variant = await p.choice(
|
|
33
138
|
"Which template do you want to start from?",
|
|
34
139
|
[
|
|
35
|
-
{
|
|
140
|
+
{
|
|
141
|
+
value: "generic",
|
|
142
|
+
label: "Generic",
|
|
143
|
+
hint: "Default. Good for most repos.",
|
|
144
|
+
},
|
|
36
145
|
{
|
|
37
146
|
value: "frontend-app",
|
|
38
147
|
label: "Frontend web app",
|
|
@@ -49,145 +158,179 @@ async function init(args) {
|
|
|
49
158
|
hint: "Schema, errors, auth, telemetry.",
|
|
50
159
|
},
|
|
51
160
|
],
|
|
52
|
-
{ default: 0 }
|
|
161
|
+
{ default: 0 },
|
|
53
162
|
);
|
|
54
163
|
}
|
|
55
164
|
if (!VARIANTS[variant]) {
|
|
56
165
|
throw new Error(
|
|
57
|
-
`Unknown variant "${variant}". Choose one of: ${Object.keys(VARIANTS).join(", ")}
|
|
166
|
+
`Unknown variant "${variant}". Choose one of: ${Object.keys(VARIANTS).join(", ")}.`,
|
|
58
167
|
);
|
|
59
168
|
}
|
|
60
169
|
|
|
61
|
-
|
|
170
|
+
const projectContext = await askProjectContext(p);
|
|
62
171
|
process.stdout.write(
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
172
|
+
"\n" +
|
|
173
|
+
c.green("✓ ") +
|
|
174
|
+
"Great, that's the foundational context.\n",
|
|
66
175
|
);
|
|
67
176
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
default:
|
|
78
|
-
"Document a real gap here (e.g. screen reader users on the dashboard; Spanish-language onboarding).",
|
|
79
|
-
}
|
|
80
|
-
);
|
|
81
|
-
const distribution = await p.text(
|
|
82
|
-
"Where is this software used? (geography, devices, network, assistive tech)",
|
|
83
|
-
{
|
|
84
|
-
default:
|
|
85
|
-
"Web/iOS/Android, primary regions, languages, common assistive technologies.",
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
const stakes = await p.text(
|
|
89
|
-
"What happens when this software excludes someone?",
|
|
90
|
-
{
|
|
91
|
-
default:
|
|
92
|
-
"Describe the real-world cost of exclusion (loss of access to healthcare, employment, civic services, etc.).",
|
|
93
|
-
}
|
|
94
|
-
);
|
|
177
|
+
// --- Design Decisions (opt-in) ---------------------------------------
|
|
178
|
+
let designDecisions = {};
|
|
179
|
+
const wantsDecisions = args.yes
|
|
180
|
+
? false
|
|
181
|
+
: await p.confirm(
|
|
182
|
+
`Want to also fill in the Design Decisions questionnaire? (${TOTAL_QUESTIONS} short questions, all skippable)`,
|
|
183
|
+
{ default: true },
|
|
184
|
+
);
|
|
95
185
|
|
|
96
|
-
|
|
186
|
+
if (wantsDecisions) {
|
|
187
|
+
designDecisions = await runQuestionnaire(p);
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
c.green("✓ ") + "Done. Folding your answers into the doc.\n",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
97
192
|
|
|
98
|
-
const
|
|
99
|
-
default: "An accountable person or team",
|
|
100
|
-
});
|
|
101
|
-
const cadence = await p.choice(
|
|
102
|
-
"How often will this file be reviewed?",
|
|
103
|
-
[
|
|
104
|
-
{ value: "Monthly.", label: "Monthly" },
|
|
105
|
-
{ value: "Quarterly.", label: "Quarterly", hint: "Recommended." },
|
|
106
|
-
{ value: "Twice a year.", label: "Twice a year" },
|
|
107
|
-
{ value: "Annually (minimum).", label: "Annually (minimum)" },
|
|
108
|
-
],
|
|
109
|
-
{ default: 1 }
|
|
110
|
-
);
|
|
111
|
-
const feedback = await p.text(
|
|
112
|
-
"How can users and contributors report exclusionary patterns?",
|
|
113
|
-
{
|
|
114
|
-
default:
|
|
115
|
-
"Open an issue on this repository, or contact the owner directly.",
|
|
116
|
-
}
|
|
117
|
-
);
|
|
193
|
+
const maintenance = await askMaintenance(p);
|
|
118
194
|
|
|
119
195
|
// --- output path ------------------------------------------------------
|
|
120
|
-
const outPath = path.resolve(
|
|
121
|
-
|
|
122
|
-
args.out || "INCLUSION.md"
|
|
123
|
-
);
|
|
196
|
+
const outPath = path.resolve(process.cwd(), args.out || "INCLUSION.md");
|
|
197
|
+
const allAnswers = { ...projectContext, ...maintenance, designDecisions };
|
|
124
198
|
|
|
125
199
|
if (fs.existsSync(outPath) && !args.force) {
|
|
126
|
-
const
|
|
127
|
-
`${path.relative(process.cwd(), outPath) || outPath} already exists.
|
|
128
|
-
|
|
200
|
+
const choice = await p.choice(
|
|
201
|
+
`${path.relative(process.cwd(), outPath) || outPath} already exists. What now?`,
|
|
202
|
+
[
|
|
203
|
+
{
|
|
204
|
+
value: "update",
|
|
205
|
+
label: "Update it in place",
|
|
206
|
+
hint: "Keeps your edits; only rewrites Section 1 + Section 12.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
value: "overwrite",
|
|
210
|
+
label: "Overwrite with a fresh template",
|
|
211
|
+
hint: "Discards all existing content.",
|
|
212
|
+
},
|
|
213
|
+
{ value: "abort", label: "Cancel" },
|
|
214
|
+
],
|
|
215
|
+
{ default: 0 },
|
|
129
216
|
);
|
|
130
|
-
if (
|
|
217
|
+
if (choice === "abort") {
|
|
131
218
|
process.stdout.write(
|
|
132
|
-
c.yellow("\nAborted. ") + "Existing file left untouched.\n"
|
|
219
|
+
c.yellow("\nAborted. ") + "Existing file left untouched.\n",
|
|
133
220
|
);
|
|
134
221
|
p.close();
|
|
135
222
|
return;
|
|
136
223
|
}
|
|
224
|
+
if (choice === "update") {
|
|
225
|
+
const existing = fs.readFileSync(outPath, "utf8");
|
|
226
|
+
const rendered = renderGeneric(existing, allAnswers);
|
|
227
|
+
fs.writeFileSync(outPath, rendered, "utf8");
|
|
228
|
+
p.close();
|
|
229
|
+
printSuccess(outPath, rendered, "updated");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
137
232
|
}
|
|
138
233
|
|
|
139
|
-
// --- render -----------------------------------------------------------
|
|
140
234
|
const template = loadTemplate(variant);
|
|
141
|
-
const
|
|
142
|
-
product,
|
|
143
|
-
primaryUsers,
|
|
144
|
-
underservedUsers,
|
|
145
|
-
distribution,
|
|
146
|
-
stakes,
|
|
147
|
-
owner,
|
|
148
|
-
cadence,
|
|
149
|
-
feedback,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
let rendered;
|
|
153
|
-
if (variant === "generic") {
|
|
154
|
-
rendered = renderGeneric(template, answers);
|
|
155
|
-
} else {
|
|
156
|
-
rendered = prependPersonalizedContext(template, answers);
|
|
157
|
-
}
|
|
235
|
+
const rendered = renderGeneric(template, allAnswers);
|
|
158
236
|
|
|
159
237
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
160
238
|
fs.writeFileSync(outPath, rendered, "utf8");
|
|
161
239
|
|
|
162
240
|
p.close();
|
|
241
|
+
printSuccess(outPath, rendered, "wrote");
|
|
242
|
+
} finally {
|
|
243
|
+
p.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
163
246
|
|
|
164
|
-
|
|
247
|
+
async function update(args) {
|
|
248
|
+
await welcome(args);
|
|
249
|
+
|
|
250
|
+
const outPath = path.resolve(process.cwd(), args.out || "INCLUSION.md");
|
|
251
|
+
if (!fs.existsSync(outPath)) {
|
|
165
252
|
process.stdout.write(
|
|
166
|
-
"
|
|
167
|
-
c.
|
|
168
|
-
|
|
169
|
-
c.dim(
|
|
170
|
-
"
|
|
253
|
+
c.yellow("Hmm, I can't find an INCLUSION.md at ") +
|
|
254
|
+
c.bold(path.relative(process.cwd(), outPath) || outPath) +
|
|
255
|
+
".\n" +
|
|
256
|
+
c.dim("Run ") +
|
|
257
|
+
c.cyan("npx inclusion-md init") +
|
|
258
|
+
c.dim(" to create one, or pass ") +
|
|
259
|
+
c.cyan("--out <path>") +
|
|
260
|
+
c.dim(" to point at the right location.\n"),
|
|
171
261
|
);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
172
264
|
|
|
173
|
-
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
"Updating " +
|
|
267
|
+
c.bold(path.relative(process.cwd(), outPath) || outPath) +
|
|
268
|
+
".\n" +
|
|
269
|
+
c.dim(
|
|
270
|
+
"I'll re-ask the questionnaire and rewrite Section 1 (Project Context) + Section 12 (Maintenance).\n" +
|
|
271
|
+
"Everything else - including any edits you've made - is preserved.\n",
|
|
272
|
+
) +
|
|
273
|
+
"\n",
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const p = createPrompter({ acceptDefaults: !!args.yes });
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const proceed = await p.confirm("Cool to proceed?", { default: true });
|
|
280
|
+
if (!proceed) {
|
|
281
|
+
process.stdout.write(c.yellow("\nAborted.\n"));
|
|
282
|
+
p.close();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const projectContext = await askProjectContext(p);
|
|
287
|
+
|
|
288
|
+
let designDecisions = {};
|
|
289
|
+
const wantsDecisions = args.yes
|
|
290
|
+
? false
|
|
291
|
+
: await p.confirm("Update the Design Decisions section too?", {
|
|
292
|
+
default: true,
|
|
293
|
+
});
|
|
294
|
+
if (wantsDecisions) {
|
|
295
|
+
designDecisions = await runQuestionnaire(p);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const maintenance = await askMaintenance(p);
|
|
299
|
+
|
|
300
|
+
const existing = fs.readFileSync(outPath, "utf8");
|
|
301
|
+
const rendered = renderGeneric(existing, {
|
|
302
|
+
...projectContext,
|
|
303
|
+
...maintenance,
|
|
304
|
+
designDecisions,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
fs.writeFileSync(outPath, rendered, "utf8");
|
|
308
|
+
p.close();
|
|
309
|
+
printSuccess(outPath, rendered, "updated");
|
|
174
310
|
} finally {
|
|
175
311
|
p.close();
|
|
176
312
|
}
|
|
177
313
|
}
|
|
178
314
|
|
|
179
|
-
function prependPersonalizedContext(template, answers) {
|
|
180
|
-
// For non-generic variants, the canonical template already has its own
|
|
181
|
-
// Section 1. We replace it with the user's answers.
|
|
182
|
-
return renderGeneric(template, answers);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
315
|
function formatBytes(n) {
|
|
186
316
|
if (n < 1024) return `${n} B`;
|
|
187
317
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
188
318
|
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
189
319
|
}
|
|
190
320
|
|
|
321
|
+
function printSuccess(outPath, rendered, verb) {
|
|
322
|
+
const rel = path.relative(process.cwd(), outPath) || outPath;
|
|
323
|
+
process.stdout.write(
|
|
324
|
+
"\n" +
|
|
325
|
+
c.green("✓ ") +
|
|
326
|
+
c.bold(`${verb === "updated" ? "Updated" : "Wrote"} ${rel}`) +
|
|
327
|
+
c.dim(` (${formatBytes(Buffer.byteLength(rendered))})`) +
|
|
328
|
+
"\n\n",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
printNextSteps(rel);
|
|
332
|
+
}
|
|
333
|
+
|
|
191
334
|
function printNextSteps(file) {
|
|
192
335
|
process.stdout.write(
|
|
193
336
|
c.bold("Next steps") +
|
|
@@ -200,11 +343,11 @@ function printNextSteps(file) {
|
|
|
200
343
|
` Always read and follow /INCLUSION.md.\n\n` +
|
|
201
344
|
` ${c.dim("# Claude Code - CLAUDE.md")}\n` +
|
|
202
345
|
` Read /INCLUSION.md and apply its review prompts.\n\n` +
|
|
203
|
-
` 3. Commit it. Treat it like the rest of your engineering docs.\n
|
|
204
|
-
c.
|
|
205
|
-
|
|
206
|
-
)
|
|
346
|
+
` 3. Commit it. Treat it like the rest of your engineering docs.\n` +
|
|
347
|
+
` 4. Run ${c.cyan("npx inclusion-md update")} on a cadence (quarterly is a good default).\n\n` +
|
|
348
|
+
c.dim(`Companion essay: ${ESSAY_URL}\n`) +
|
|
349
|
+
c.dim(`Repo: ${REPO_URL}\n`),
|
|
207
350
|
);
|
|
208
351
|
}
|
|
209
352
|
|
|
210
|
-
module.exports = { init };
|
|
353
|
+
module.exports = { init, update };
|
package/lib/prompt.js
CHANGED
|
@@ -17,13 +17,26 @@ function createPrompter({ acceptDefaults = false } = {}) {
|
|
|
17
17
|
throw err;
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
// Track the in-flight ask() so a single 'close' listener can reject it.
|
|
21
|
+
// Registering rl.once("close", ...) inside ask() leaked one listener per
|
|
22
|
+
// prompt and triggered MaxListenersExceededWarning around question ~11.
|
|
23
|
+
let pendingReject = null;
|
|
24
|
+
rl.on("close", () => {
|
|
25
|
+
if (pendingReject) {
|
|
26
|
+
const err = new Error("Cancelled by user");
|
|
27
|
+
err.code = "USER_CANCELLED";
|
|
28
|
+
const reject = pendingReject;
|
|
29
|
+
pendingReject = null;
|
|
30
|
+
reject(err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
20
34
|
function ask(question) {
|
|
21
35
|
return new Promise((resolve, reject) => {
|
|
22
|
-
|
|
23
|
-
rl.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
reject(err);
|
|
36
|
+
pendingReject = reject;
|
|
37
|
+
rl.question(question, (answer) => {
|
|
38
|
+
pendingReject = null;
|
|
39
|
+
resolve(answer);
|
|
27
40
|
});
|
|
28
41
|
});
|
|
29
42
|
}
|
|
@@ -36,7 +49,9 @@ function createPrompter({ acceptDefaults = false } = {}) {
|
|
|
36
49
|
async function text(label, { default: def = "", required = false } = {}) {
|
|
37
50
|
if (acceptDefaults) return def;
|
|
38
51
|
while (true) {
|
|
39
|
-
const raw = await ask(
|
|
52
|
+
const raw = await ask(
|
|
53
|
+
`${c.cyan("?")} ${c.bold(label)}${fmtDefault(def)}: `,
|
|
54
|
+
);
|
|
40
55
|
const val = (raw || "").trim();
|
|
41
56
|
if (val) return val;
|
|
42
57
|
if (def) return def;
|
|
@@ -49,10 +64,12 @@ function createPrompter({ acceptDefaults = false } = {}) {
|
|
|
49
64
|
if (acceptDefaults) return def;
|
|
50
65
|
process.stdout.write(
|
|
51
66
|
`${c.cyan("?")} ${c.bold(label)}\n` +
|
|
52
|
-
c.dim(" (one item per line; empty line to finish")
|
|
67
|
+
c.dim(" (one item per line; empty line to finish"),
|
|
53
68
|
);
|
|
54
69
|
if (def)
|
|
55
|
-
process.stdout.write(
|
|
70
|
+
process.stdout.write(
|
|
71
|
+
c.dim(`; press enter immediately to accept default`),
|
|
72
|
+
);
|
|
56
73
|
process.stdout.write(c.dim(")\n"));
|
|
57
74
|
const lines = [];
|
|
58
75
|
while (true) {
|
|
@@ -73,18 +90,24 @@ function createPrompter({ acceptDefaults = false } = {}) {
|
|
|
73
90
|
process.stdout.write(
|
|
74
91
|
` ${marker} ${c.bold(String(i + 1))}) ${opt.label}` +
|
|
75
92
|
(opt.hint ? c.dim(` - ${opt.hint}`) : "") +
|
|
76
|
-
"\n"
|
|
93
|
+
"\n",
|
|
77
94
|
);
|
|
78
95
|
});
|
|
79
96
|
while (true) {
|
|
80
|
-
const raw = await ask(
|
|
97
|
+
const raw = await ask(
|
|
98
|
+
c.dim(` choose 1-${options.length} `) +
|
|
99
|
+
fmtDefault(defIndex + 1) +
|
|
100
|
+
": ",
|
|
101
|
+
);
|
|
81
102
|
const val = (raw || "").trim();
|
|
82
103
|
if (val === "") return options[defIndex].value;
|
|
83
104
|
const n = parseInt(val, 10);
|
|
84
105
|
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
85
106
|
return options[n - 1].value;
|
|
86
107
|
}
|
|
87
|
-
process.stdout.write(
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
c.yellow(` Please enter a number 1-${options.length}.\n`),
|
|
110
|
+
);
|
|
88
111
|
}
|
|
89
112
|
}
|
|
90
113
|
|
|
@@ -92,7 +115,9 @@ function createPrompter({ acceptDefaults = false } = {}) {
|
|
|
92
115
|
if (acceptDefaults) return def;
|
|
93
116
|
const hint = def ? "Y/n" : "y/N";
|
|
94
117
|
while (true) {
|
|
95
|
-
const raw = await ask(
|
|
118
|
+
const raw = await ask(
|
|
119
|
+
`${c.cyan("?")} ${c.bold(label)} ${c.dim(`(${hint})`)}: `,
|
|
120
|
+
);
|
|
96
121
|
const val = (raw || "").trim().toLowerCase();
|
|
97
122
|
if (val === "") return def;
|
|
98
123
|
if (val === "y" || val === "yes") return true;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const c = require("./colors");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Design Decisions questionnaire. 13 questions across 6 groups, each
|
|
7
|
+
* skippable. Surfaces the real tradeoffs your product makes - who you've
|
|
8
|
+
* intentionally designed for, who you know is excluded, and why.
|
|
9
|
+
*
|
|
10
|
+
* Grounded in:
|
|
11
|
+
* - ABLEIST: Measuring Ableist Harms in LLMs (arxiv.org/abs/2510.10998)
|
|
12
|
+
* - Centering Disability Perspectives in LLM Research and Design (ACM)
|
|
13
|
+
* - Kat Holmes, Mismatch: How Inclusion Shapes Design (MIT Press)
|
|
14
|
+
*
|
|
15
|
+
* The point isn't perfect answers. It's conscious, documented tradeoffs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const GROUPS = [
|
|
19
|
+
{
|
|
20
|
+
title: "Group 1: Core Assumptions",
|
|
21
|
+
blurb:
|
|
22
|
+
"What you assume about the people using your product. These shape every other decision.",
|
|
23
|
+
questions: [
|
|
24
|
+
{
|
|
25
|
+
key: "primaryAudience",
|
|
26
|
+
label: "Who do you primarily build for?",
|
|
27
|
+
hint:
|
|
28
|
+
"Describe assumptions like language fluency, device type, network speed, technical expertise. " +
|
|
29
|
+
"Example: 'People with reliable internet, modern browsers, English fluency.'",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "implicitDefaults",
|
|
33
|
+
label:
|
|
34
|
+
"What 'default user' assumptions live inside your product today?",
|
|
35
|
+
hint:
|
|
36
|
+
"The non-disabled, English-fluent, neurotypical user with a fast connection is a statistical artifact, not a person. " +
|
|
37
|
+
"Where does that default show up in yours?",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "Group 2: Authentication & Access",
|
|
43
|
+
blurb: "Where exclusion often happens first.",
|
|
44
|
+
questions: [
|
|
45
|
+
{
|
|
46
|
+
key: "authMethods",
|
|
47
|
+
label:
|
|
48
|
+
"How do people get into your product? What auth methods do you support?",
|
|
49
|
+
hint:
|
|
50
|
+
"Examples: email/password, email + SMS, social login, passkey, SSO, magic link.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "authExcluded",
|
|
54
|
+
label:
|
|
55
|
+
"Is there anyone who can't use your authentication system right now?",
|
|
56
|
+
hint:
|
|
57
|
+
"Don't say 'no one' - there's always someone. Examples: people without reliable email, " +
|
|
58
|
+
"people without a phone, people in regions without SMS, people who share a device.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "authReason",
|
|
62
|
+
label: "Why did you choose that authentication method?",
|
|
63
|
+
hint:
|
|
64
|
+
"Be honest: easiest to implement, most widely supported, security requirement, industry standard.",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: "Group 3: Information Collection",
|
|
70
|
+
blurb:
|
|
71
|
+
"What you ask for shapes who can show up. Required vs. optional matters.",
|
|
72
|
+
questions: [
|
|
73
|
+
{
|
|
74
|
+
key: "infoCollected",
|
|
75
|
+
label: "What personal information do you ask people to provide?",
|
|
76
|
+
hint:
|
|
77
|
+
"Examples: name, email, phone, DOB, location, video ID, gender (and what options?), disability status. " +
|
|
78
|
+
"Note required vs. optional.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "infoRationale",
|
|
82
|
+
label: "For any required personal information, why is it required?",
|
|
83
|
+
hint:
|
|
84
|
+
"For each field: could we work without this? Example: 'Email required for notifications - could SMS work instead?'",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "Group 4: Interaction Model",
|
|
90
|
+
blurb: "How people actually use the product.",
|
|
91
|
+
questions: [
|
|
92
|
+
{
|
|
93
|
+
key: "interactionModes",
|
|
94
|
+
label: "How do people interact with your product?",
|
|
95
|
+
hint:
|
|
96
|
+
"Pick all that apply: text, voice, visual/color-based, video, synchronous, asynchronous, " +
|
|
97
|
+
"touch, keyboard, motor coordination, screen reader.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "unsupportedPatterns",
|
|
101
|
+
label: "Are there interaction patterns you consciously don't support?",
|
|
102
|
+
hint:
|
|
103
|
+
"Examples: video verification (cost), voice (bias concerns), offline mode, keyboard-only nav, " +
|
|
104
|
+
"text-to-speech, RTL languages, slow networks, mobile-only.",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: "Group 5: Communication & Language",
|
|
110
|
+
blurb: "Tone and language are inclusion choices, not aesthetic ones.",
|
|
111
|
+
questions: [
|
|
112
|
+
{
|
|
113
|
+
key: "languages",
|
|
114
|
+
label: "What language(s) do you currently support?",
|
|
115
|
+
hint:
|
|
116
|
+
"English only, English + [list], multilingual from day 1, plan to add languages in [timeframe].",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "communicationStyle",
|
|
120
|
+
label: "What communication style(s) are you optimizing for?",
|
|
121
|
+
hint:
|
|
122
|
+
"Formal/professional, casual/friendly, technical, plain-language, direct. " +
|
|
123
|
+
"Example: 'Casual + friendly, but avoid metaphors that might be ableist.'",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
title: "Group 6: Edge Cases & Intersections",
|
|
129
|
+
blurb:
|
|
130
|
+
"Specific scenarios where the product breaks - or where unexpected users show up.",
|
|
131
|
+
questions: [
|
|
132
|
+
{
|
|
133
|
+
key: "knownBreakage",
|
|
134
|
+
label: "What's one scenario where your product might not work?",
|
|
135
|
+
hint:
|
|
136
|
+
"Be specific, not vague. Example: 'People in rural areas with <1 Mbps internet' or 'Left-handed users on touch devices.'",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "unexpectedUsers",
|
|
140
|
+
label: "Who might use your product in ways you didn't expect?",
|
|
141
|
+
hint:
|
|
142
|
+
"Examples: older relatives, people in different countries, people with low vision zooming, " +
|
|
143
|
+
"screen reader users, people with limited energy, teams in different time zones, non-native speakers.",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const TOTAL_QUESTIONS = GROUPS.reduce((n, g) => n + g.questions.length, 0);
|
|
150
|
+
|
|
151
|
+
async function runQuestionnaire(p) {
|
|
152
|
+
process.stdout.write(
|
|
153
|
+
"\n" +
|
|
154
|
+
c.bold("Design Decisions") +
|
|
155
|
+
" " +
|
|
156
|
+
c.dim(`(${TOTAL_QUESTIONS} questions, all skippable)`) +
|
|
157
|
+
"\n" +
|
|
158
|
+
c.dim(
|
|
159
|
+
"These surface the real tradeoffs your product makes. Skip any with an empty answer.\n",
|
|
160
|
+
) +
|
|
161
|
+
"\n",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const answers = {};
|
|
165
|
+
let asked = 0;
|
|
166
|
+
|
|
167
|
+
for (let g = 0; g < GROUPS.length; g++) {
|
|
168
|
+
const group = GROUPS[g];
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
c.cyan(group.title) +
|
|
171
|
+
"\n" +
|
|
172
|
+
c.dim(` ${group.blurb}`) +
|
|
173
|
+
"\n\n",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const q of group.questions) {
|
|
177
|
+
asked++;
|
|
178
|
+
process.stdout.write(
|
|
179
|
+
c.dim(`Question ${asked} of ${TOTAL_QUESTIONS}`) + "\n",
|
|
180
|
+
);
|
|
181
|
+
if (q.hint) {
|
|
182
|
+
process.stdout.write(c.dim(` ${q.hint}`) + "\n");
|
|
183
|
+
}
|
|
184
|
+
const val = await p.text(q.label, { default: "", required: false });
|
|
185
|
+
if (val) {
|
|
186
|
+
answers[q.key] = val;
|
|
187
|
+
} else {
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
c.dim(" (skipped - you can fill this in later)\n"),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (g < GROUPS.length - 1) {
|
|
196
|
+
process.stdout.write(c.dim(" ---\n\n"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return answers;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function hasAnyAnswers(designDecisions) {
|
|
204
|
+
return designDecisions && Object.keys(designDecisions).length > 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderDesignDecisionsMarkdown(answers) {
|
|
208
|
+
if (!hasAnyAnswers(answers)) return "";
|
|
209
|
+
|
|
210
|
+
const lines = [];
|
|
211
|
+
lines.push("### 1.B Design Decisions");
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(
|
|
214
|
+
"Conscious tradeoffs documented as part of this project's inclusion posture. " +
|
|
215
|
+
"These are not aspirations - they're current realities, surfaced so reviewers " +
|
|
216
|
+
"(human and AI) can flag when generated output assumes otherwise.",
|
|
217
|
+
);
|
|
218
|
+
lines.push("");
|
|
219
|
+
|
|
220
|
+
const sectionMap = [
|
|
221
|
+
{
|
|
222
|
+
heading: "**Core assumptions**",
|
|
223
|
+
keys: [
|
|
224
|
+
["primaryAudience", "Primary audience"],
|
|
225
|
+
["implicitDefaults", "Implicit defaults"],
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
heading: "**Authentication & access**",
|
|
230
|
+
keys: [
|
|
231
|
+
["authMethods", "Methods supported"],
|
|
232
|
+
["authExcluded", "Known exclusion"],
|
|
233
|
+
["authReason", "Reason for choice"],
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
heading: "**Information collection**",
|
|
238
|
+
keys: [
|
|
239
|
+
["infoCollected", "Information collected"],
|
|
240
|
+
["infoRationale", "Why it's required"],
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
heading: "**Interaction model**",
|
|
245
|
+
keys: [
|
|
246
|
+
["interactionModes", "Interaction modes"],
|
|
247
|
+
["unsupportedPatterns", "Patterns not supported"],
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
heading: "**Communication & language**",
|
|
252
|
+
keys: [
|
|
253
|
+
["languages", "Languages supported"],
|
|
254
|
+
["communicationStyle", "Communication style"],
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
heading: "**Edge cases & intersections**",
|
|
259
|
+
keys: [
|
|
260
|
+
["knownBreakage", "Known breakage scenarios"],
|
|
261
|
+
["unexpectedUsers", "Unexpected users"],
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
for (const section of sectionMap) {
|
|
267
|
+
const entries = section.keys.filter(([k]) => answers[k]);
|
|
268
|
+
if (entries.length === 0) continue;
|
|
269
|
+
lines.push(section.heading);
|
|
270
|
+
for (const [key, label] of entries) {
|
|
271
|
+
lines.push(`- _${label}:_ ${answers[key]}`);
|
|
272
|
+
}
|
|
273
|
+
lines.push("");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
runQuestionnaire,
|
|
281
|
+
renderDesignDecisionsMarkdown,
|
|
282
|
+
hasAnyAnswers,
|
|
283
|
+
GROUPS,
|
|
284
|
+
TOTAL_QUESTIONS,
|
|
285
|
+
};
|
package/lib/template.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
+
const {
|
|
7
|
+
renderDesignDecisionsMarkdown,
|
|
8
|
+
hasAnyAnswers,
|
|
9
|
+
} = require("./questionnaire");
|
|
10
|
+
|
|
6
11
|
const VARIANTS = {
|
|
7
12
|
generic: "INCLUSION.md",
|
|
8
13
|
"frontend-app": "examples/frontend-app/INCLUSION.md",
|
|
@@ -14,7 +19,7 @@ function loadTemplate(variant) {
|
|
|
14
19
|
const rel = VARIANTS[variant];
|
|
15
20
|
if (!rel) {
|
|
16
21
|
throw new Error(
|
|
17
|
-
`Unknown variant "${variant}". Choose one of: ${Object.keys(VARIANTS).join(", ")}
|
|
22
|
+
`Unknown variant "${variant}". Choose one of: ${Object.keys(VARIANTS).join(", ")}.`,
|
|
18
23
|
);
|
|
19
24
|
}
|
|
20
25
|
// Resolve relative to the package root (one level up from /lib).
|
|
@@ -36,17 +41,33 @@ function renderGeneric(template, answers) {
|
|
|
36
41
|
let out = template;
|
|
37
42
|
|
|
38
43
|
// --- Section 1: Project Context -----------------------------------------
|
|
44
|
+
const projectContextLines = [
|
|
45
|
+
`## 1. Project Context`,
|
|
46
|
+
``,
|
|
47
|
+
`### 1.A Overview`,
|
|
48
|
+
``,
|
|
49
|
+
`- **Product:** ${answers.product}`,
|
|
50
|
+
`- **Primary users:** ${answers.primaryUsers}`,
|
|
51
|
+
`- **Known underserved users:** ${answers.underservedUsers}`,
|
|
52
|
+
`- **Distribution context:** ${answers.distribution}`,
|
|
53
|
+
`- **Stakes:** ${answers.stakes}`,
|
|
54
|
+
``,
|
|
55
|
+
`Generated code, copy, and interaction patterns should be evaluated against the`,
|
|
56
|
+
`context above before being merged.`,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const designDecisionsBlock = hasAnyAnswers(answers.designDecisions)
|
|
60
|
+
? "\n\n" + renderDesignDecisionsMarkdown(answers.designDecisions).trimEnd()
|
|
61
|
+
: "";
|
|
62
|
+
|
|
39
63
|
const newProjectContext =
|
|
40
|
-
|
|
41
|
-
`- **Product:** ${answers.product}\n` +
|
|
42
|
-
`- **Primary users:** ${answers.primaryUsers}\n` +
|
|
43
|
-
`- **Known underserved users:** ${answers.underservedUsers}\n` +
|
|
44
|
-
`- **Distribution context:** ${answers.distribution}\n` +
|
|
45
|
-
`- **Stakes:** ${answers.stakes}\n\n` +
|
|
46
|
-
`Generated code, copy, and interaction patterns should be evaluated against the\n` +
|
|
47
|
-
`context above before being merged.`;
|
|
64
|
+
projectContextLines.join("\n") + designDecisionsBlock;
|
|
48
65
|
|
|
49
|
-
out = replaceSection(
|
|
66
|
+
out = replaceSection(
|
|
67
|
+
out,
|
|
68
|
+
/^## 1\. Project Context[\s\S]*?(?=^---\s*$)/m,
|
|
69
|
+
newProjectContext + "\n\n",
|
|
70
|
+
);
|
|
50
71
|
|
|
51
72
|
// --- Section 12: Maintenance --------------------------------------------
|
|
52
73
|
const newMaintenance =
|
|
@@ -57,7 +78,11 @@ function renderGeneric(template, answers) {
|
|
|
57
78
|
` [\`CHANGELOG.md\`](./CHANGELOG.md) or in repository releases.\n` +
|
|
58
79
|
`- **Feedback:** ${answers.feedback}`;
|
|
59
80
|
|
|
60
|
-
out = replaceSection(
|
|
81
|
+
out = replaceSection(
|
|
82
|
+
out,
|
|
83
|
+
/^## 12\. Maintenance[\s\S]*?(?=^---\s*$)/m,
|
|
84
|
+
newMaintenance + "\n\n",
|
|
85
|
+
);
|
|
61
86
|
|
|
62
87
|
return out;
|
|
63
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inclusion-md",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Scaffold an INCLUSION.md - a context engineering doc that gives AI coding assistants inclusion-oriented guidance during code generation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"inclusion",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"node": ">=16"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "node
|
|
44
|
+
"test": "node --test test/",
|
|
45
|
+
"help": "node bin/inclusion-md.js --help"
|
|
45
46
|
}
|
|
46
47
|
}
|