@oddessentials/repo-standards 4.1.0 → 4.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.
@@ -0,0 +1,82 @@
1
+ // scripts/detect-bazel.ts
2
+
3
+ /**
4
+ * Standalone Bazel detection utility.
5
+ * Does NOT require Bazel to be installed.
6
+ * Only checks repo-root markers to avoid false positives from vendored deps.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+
12
+ export interface BazelDetectionResult {
13
+ /** Whether Bazel markers were detected at repo root */
14
+ detected: boolean;
15
+ /** Bazel mode: bzlmod (MODULE.bazel) or workspace (WORKSPACE*) */
16
+ mode?: "bzlmod" | "workspace";
17
+ /** List of detected marker files */
18
+ markers: string[];
19
+ }
20
+
21
+ /** Root-level markers that indicate a Bazel repo */
22
+ const ROOT_MARKERS = {
23
+ bzlmod: ["MODULE.bazel"],
24
+ workspace: ["WORKSPACE.bazel", "WORKSPACE"],
25
+ };
26
+
27
+ /** Optional markers that support Bazel but don't trigger detection alone */
28
+ const OPTIONAL_MARKERS = [".bazelrc", ".bazelversion"];
29
+
30
+ /**
31
+ * Detect Bazel at repo root only.
32
+ * Does NOT scan subdirectories for BUILD files (avoids vendored deps false positives).
33
+ *
34
+ * @param repoRoot - Absolute path to repository root
35
+ * @returns Detection result with mode and found markers
36
+ */
37
+ export function detectBazel(repoRoot: string): BazelDetectionResult {
38
+ const foundMarkers: string[] = [];
39
+ let mode: "bzlmod" | "workspace" | undefined;
40
+
41
+ // Check bzlmod first (preferred)
42
+ for (const marker of ROOT_MARKERS.bzlmod) {
43
+ if (fs.existsSync(path.join(repoRoot, marker))) {
44
+ foundMarkers.push(marker);
45
+ mode = "bzlmod";
46
+ }
47
+ }
48
+
49
+ // Check workspace if no bzlmod
50
+ if (!mode) {
51
+ for (const marker of ROOT_MARKERS.workspace) {
52
+ if (fs.existsSync(path.join(repoRoot, marker))) {
53
+ foundMarkers.push(marker);
54
+ mode = "workspace";
55
+ break; // Only need one workspace marker
56
+ }
57
+ }
58
+ }
59
+
60
+ // Check optional markers (don't affect detection, just informational)
61
+ for (const marker of OPTIONAL_MARKERS) {
62
+ if (fs.existsSync(path.join(repoRoot, marker))) {
63
+ foundMarkers.push(marker);
64
+ }
65
+ }
66
+
67
+ return {
68
+ detected: mode !== undefined,
69
+ mode,
70
+ markers: foundMarkers,
71
+ };
72
+ }
73
+
74
+ // CLI entrypoint for manual testing
75
+ if (
76
+ import.meta.url.startsWith("file:") &&
77
+ process.argv[1]?.includes("detect-bazel")
78
+ ) {
79
+ const targetDir = process.argv[2] || process.cwd();
80
+ const result = detectBazel(path.resolve(targetDir));
81
+ console.log(JSON.stringify(result, null, 2));
82
+ }
@@ -0,0 +1,174 @@
1
+ // scripts/generate-instructions.ts
2
+ //
3
+ // Generates instructions.md from a stack+CI specific JSON file.
4
+ // Usage: npx ts-node-esm scripts/generate-instructions.ts [config-file]
5
+ // Example: npm run generate:instructions standards.python.github-actions.json
6
+ // Default: config/standards.typescript-js.github-actions.json
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ interface StackHints {
12
+ exampleTools?: string[];
13
+ exampleConfigFiles?: string[];
14
+ notes?: string;
15
+ verification?: string;
16
+ requiredFiles?: string[];
17
+ optionalFiles?: string[];
18
+ requiredScripts?: string[];
19
+ }
20
+
21
+ interface ChecklistItem {
22
+ id: string;
23
+ label: string;
24
+ description: string;
25
+ ciHints?: Record<string, { job?: string; stage?: string }>;
26
+ stack?: StackHints;
27
+ }
28
+
29
+ interface StackChecklistJson {
30
+ version: number;
31
+ stack: string;
32
+ stackLabel: string;
33
+ ciSystems: string[];
34
+ checklist: {
35
+ core: ChecklistItem[];
36
+ recommended: ChecklistItem[];
37
+ optionalEnhancements: ChecklistItem[];
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Generate 2-5 high-level bullets from a checklist item.
43
+ */
44
+ function generateBullets(item: ChecklistItem): string[] {
45
+ const bullets: string[] = [];
46
+ const stack = item.stack;
47
+
48
+ // Bullet 1: Core purpose from description
49
+ bullets.push(item.description);
50
+
51
+ // Bullet 2: Required files if any
52
+ if (stack?.requiredFiles?.length) {
53
+ const files = stack.requiredFiles.join(", ");
54
+ bullets.push(`Ensure ${files} exists in the repository.`);
55
+ }
56
+
57
+ // Bullet 3: Optional files mention
58
+ if (stack?.optionalFiles?.length) {
59
+ const files = stack.optionalFiles.slice(0, 3).join(", ");
60
+ const more = stack.optionalFiles.length > 3 ? " and others" : "";
61
+ bullets.push(`Consider adding ${files}${more} if applicable.`);
62
+ }
63
+
64
+ // Bullet 4: Required scripts
65
+ if (stack?.requiredScripts?.length) {
66
+ const scripts = stack.requiredScripts.map((s) => `\`${s}\``).join(", ");
67
+ bullets.push(`Define a ${scripts} script or equivalent command.`);
68
+ }
69
+
70
+ // Bullet 5: Notes-based guidance (if short enough)
71
+ if (stack?.notes && stack.notes.length < 150) {
72
+ bullets.push(stack.notes);
73
+ }
74
+
75
+ // Limit to 5 bullets
76
+ return bullets.slice(0, 5);
77
+ }
78
+
79
+ function generateSection(title: string, items: ChecklistItem[]): string {
80
+ if (items.length === 0) return "";
81
+
82
+ const lines: string[] = [];
83
+ lines.push(`## ${title}`);
84
+ lines.push("");
85
+
86
+ for (const item of items) {
87
+ lines.push(`### ${item.label}`);
88
+ lines.push("");
89
+
90
+ const bullets = generateBullets(item);
91
+ for (const bullet of bullets) {
92
+ lines.push(`- ${bullet}`);
93
+ }
94
+ lines.push("");
95
+ }
96
+
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function main() {
101
+ const rootDir = process.cwd();
102
+
103
+ // Accept config file as CLI argument, default to TypeScript+GitHub Actions
104
+ const configArg = process.argv[2];
105
+ let inputPath: string;
106
+
107
+ if (configArg) {
108
+ // If argument provided, resolve relative to config/ or as absolute
109
+ if (path.isAbsolute(configArg)) {
110
+ inputPath = configArg;
111
+ } else if (
112
+ configArg.startsWith("config/") ||
113
+ configArg.startsWith("config\\")
114
+ ) {
115
+ inputPath = path.join(rootDir, configArg);
116
+ } else {
117
+ inputPath = path.join(rootDir, "config", configArg);
118
+ }
119
+ } else {
120
+ inputPath = path.join(
121
+ rootDir,
122
+ "config",
123
+ "standards.typescript-js.github-actions.json",
124
+ );
125
+ }
126
+
127
+ // Derive output filename from input (e.g., standards.python.json -> instructions.python.md)
128
+ const inputBasename = path.basename(inputPath, ".json");
129
+ const outputBasename = inputBasename.replace(/^standards\./, "instructions.");
130
+ const outputPath = path.join(rootDir, `${outputBasename}.md`);
131
+
132
+ if (!fs.existsSync(inputPath)) {
133
+ console.error(`Input file not found: ${inputPath}`);
134
+ console.error("Usage: npm run generate:instructions [config-file]");
135
+ console.error(
136
+ "Example: npm run generate:instructions standards.python.github-actions.json",
137
+ );
138
+ process.exit(1);
139
+ }
140
+
141
+ const raw = fs.readFileSync(inputPath, "utf8");
142
+ const data: StackChecklistJson = JSON.parse(raw);
143
+
144
+ const lines: string[] = [];
145
+
146
+ // Header
147
+ lines.push("# Repository Standards Instructions");
148
+ lines.push("");
149
+ lines.push(`> Auto-generated from \`${path.relative(rootDir, inputPath)}\``);
150
+ lines.push(`> Stack: ${data.stackLabel} | CI: ${data.ciSystems.join(", ")}`);
151
+ lines.push("");
152
+ lines.push(
153
+ "This document provides high-level guidance for an autonomous coding agent to bring a repository into compliance with the defined standards.",
154
+ );
155
+ lines.push("");
156
+
157
+ // Generate sections
158
+ lines.push(generateSection("Core Requirements", data.checklist.core));
159
+ lines.push(
160
+ generateSection("Recommended Practices", data.checklist.recommended),
161
+ );
162
+ lines.push(
163
+ generateSection(
164
+ "Optional Enhancements",
165
+ data.checklist.optionalEnhancements,
166
+ ),
167
+ );
168
+
169
+ // Write output
170
+ fs.writeFileSync(outputPath, lines.join("\n"));
171
+ console.log(`Wrote ${outputPath}`);
172
+ }
173
+
174
+ main();
@@ -0,0 +1,247 @@
1
+ // scripts/generate-standards.ts
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ type StackId = "typescript-js" | "csharp-dotnet" | "python" | "rust" | "go";
7
+ type CiSystem = "azure-devops" | "github-actions";
8
+
9
+ interface StackMeta {
10
+ label: string;
11
+ languageFamily: string;
12
+ }
13
+
14
+ interface CiHintsForSystem {
15
+ stage?: string;
16
+ job?: string;
17
+ }
18
+
19
+ type CiHints = Partial<Record<CiSystem, CiHintsForSystem>>;
20
+
21
+ /**
22
+ * Bazel execution hints for individual checklist items.
23
+ * Commands are actual Bazel invocations (e.g., "bazel test //..."),
24
+ * NOT assumed pattern labels.
25
+ */
26
+ interface BazelHints {
27
+ /** Bazel commands to run (e.g., "bazel test //...", "bazel run //tools/lint") */
28
+ commands?: string[];
29
+ /** Recommended target conventions (documentation only, not assumed to exist) */
30
+ recommendedTargets?: string[];
31
+ /** Usage notes for this check in Bazel context */
32
+ notes?: string;
33
+ }
34
+
35
+ interface StackHints {
36
+ exampleTools?: string[];
37
+ exampleConfigFiles?: string[];
38
+ notes?: string;
39
+ // Human‑readable verification instructions
40
+ verification?: string;
41
+ // Machine‑readable additions (all optional)
42
+ requiredFiles?: string[];
43
+ optionalFiles?: string[];
44
+ requiredScripts?: string[];
45
+ // Either-or file compliance: at least one of these files must exist
46
+ anyOfFiles?: string[];
47
+ // Version pinning guidance for deterministic CI
48
+ pinningNotes?: string;
49
+ machineCheck?: {
50
+ command: string;
51
+ expectExitCode?: number;
52
+ description?: string;
53
+ };
54
+ // Bazel execution hints (v3+)
55
+ bazelHints?: BazelHints;
56
+ }
57
+
58
+ interface ChecklistItemMaster {
59
+ id: string;
60
+ label: string;
61
+ description: string;
62
+ appliesTo: {
63
+ stacks: StackId[];
64
+ ciSystems?: CiSystem[];
65
+ };
66
+ ciHints?: CiHints;
67
+ stackHints?: Partial<Record<StackId, StackHints>>;
68
+ }
69
+
70
+ interface ChecklistSection {
71
+ core: ChecklistItemMaster[];
72
+ recommended: ChecklistItemMaster[];
73
+ optionalEnhancements: ChecklistItemMaster[];
74
+ }
75
+
76
+ interface MigrationStep {
77
+ step: number;
78
+ title: string;
79
+ description: string;
80
+ focusIds?: string[];
81
+ notes?: string;
82
+ }
83
+
84
+ interface Meta {
85
+ defaultCoverageThreshold?: number;
86
+ complexityChecks?: {
87
+ enabledByDefault?: boolean;
88
+ description?: string;
89
+ };
90
+ qualityGatePolicy?: {
91
+ preferSoftFailOnLegacy?: boolean;
92
+ description?: string;
93
+ };
94
+ migrationGuide?: MigrationStep[];
95
+ }
96
+
97
+ interface MasterJson {
98
+ version: number;
99
+ meta?: Meta;
100
+ ciSystems: CiSystem[];
101
+ stacks: Record<StackId, StackMeta>;
102
+ checklist: ChecklistSection;
103
+ }
104
+
105
+ interface StackItem {
106
+ id: string;
107
+ label: string;
108
+ description: string;
109
+ ciHints?: CiHints;
110
+ // For the filtered file, this is the single stack’s hints including verification
111
+ stack?: StackHints;
112
+ }
113
+
114
+ interface StackChecklistJson {
115
+ version: number;
116
+ stack: StackId;
117
+ stackLabel: string;
118
+ ciSystems: CiSystem[];
119
+ meta?: Meta;
120
+ checklist: {
121
+ core: StackItem[];
122
+ recommended: StackItem[];
123
+ optionalEnhancements: StackItem[];
124
+ };
125
+ }
126
+
127
+ function filterSectionForStackAndCi(
128
+ items: ChecklistItemMaster[],
129
+ stack: StackId,
130
+ ciSystem?: CiSystem,
131
+ ): StackItem[] {
132
+ return items
133
+ .filter((item) => {
134
+ if (!item.appliesTo.stacks.includes(stack)) return false;
135
+
136
+ if (ciSystem && item.appliesTo.ciSystems) {
137
+ return item.appliesTo.ciSystems.includes(ciSystem);
138
+ }
139
+ return true;
140
+ })
141
+ .map((item) => {
142
+ const stackHint = item.stackHints?.[stack];
143
+
144
+ const result: StackItem = {
145
+ id: item.id,
146
+ label: item.label,
147
+ description: item.description,
148
+ };
149
+
150
+ if (item.ciHints) {
151
+ if (ciSystem) {
152
+ const perSystem = item.ciHints[ciSystem];
153
+ if (perSystem) {
154
+ result.ciHints = { [ciSystem]: perSystem };
155
+ }
156
+ } else {
157
+ result.ciHints = item.ciHints;
158
+ }
159
+ }
160
+
161
+ if (stackHint) {
162
+ // Includes exampleTools, exampleConfigFiles, notes, verification
163
+ result.stack = stackHint;
164
+ }
165
+
166
+ return result;
167
+ });
168
+ }
169
+
170
+ function generateStackJson(
171
+ master: MasterJson,
172
+ stack: StackId,
173
+ ciSystem?: CiSystem,
174
+ ): StackChecklistJson {
175
+ const stackMeta = master.stacks[stack];
176
+ const ciSystems = ciSystem ? [ciSystem] : master.ciSystems;
177
+
178
+ return {
179
+ version: master.version,
180
+ stack,
181
+ stackLabel: stackMeta?.label ?? stack,
182
+ ciSystems,
183
+ meta: master.meta,
184
+ checklist: {
185
+ core: filterSectionForStackAndCi(master.checklist.core, stack, ciSystem),
186
+ recommended: filterSectionForStackAndCi(
187
+ master.checklist.recommended,
188
+ stack,
189
+ ciSystem,
190
+ ),
191
+ optionalEnhancements: filterSectionForStackAndCi(
192
+ master.checklist.optionalEnhancements,
193
+ stack,
194
+ ciSystem,
195
+ ),
196
+ },
197
+ };
198
+ }
199
+
200
+ // --- entrypoint ---
201
+ const rootDir = path.join(process.cwd());
202
+ const masterPath = path.join(rootDir, "config", "standards.json");
203
+
204
+ const raw = fs.readFileSync(masterPath, "utf8");
205
+ const master: MasterJson = JSON.parse(raw);
206
+
207
+ // args: stack [ciSystem]
208
+ // args: stack [ciSystem]
209
+ const STACK_ALIASES: Record<string, StackId> = {
210
+ dotnet: "csharp-dotnet",
211
+ csharp: "csharp-dotnet",
212
+ ts: "typescript-js",
213
+ js: "typescript-js",
214
+ python: "python",
215
+ py: "python",
216
+ "typescript-js": "typescript-js",
217
+ "csharp-dotnet": "csharp-dotnet",
218
+ rust: "rust",
219
+ rs: "rust",
220
+ go: "go",
221
+ golang: "go",
222
+ };
223
+
224
+ const rawArg = process.argv[2] || "typescript-js";
225
+ const targetStack = STACK_ALIASES[rawArg.toLowerCase()];
226
+
227
+ if (!targetStack) {
228
+ console.error(`Unknown stack: ${rawArg}`);
229
+ console.error(
230
+ `Available stacks: ${["typescript-js", "csharp-dotnet", "python", "rust", "go"].join(", ")}`,
231
+ );
232
+ process.exit(1);
233
+ }
234
+
235
+ const targetCiSystem = process.argv[3] as CiSystem | undefined;
236
+
237
+ const stackJson = generateStackJson(master, targetStack, targetCiSystem);
238
+
239
+ const outDir = path.join(rootDir, "config");
240
+ fs.mkdirSync(outDir, { recursive: true });
241
+
242
+ const ciSuffix = targetCiSystem ? `.${targetCiSystem}` : "";
243
+ const outPath = path.join(outDir, `standards.${targetStack}${ciSuffix}.json`);
244
+
245
+ fs.writeFileSync(outPath, JSON.stringify(stackJson, null, 2) + "\n");
246
+
247
+ console.log(`Wrote ${outPath}`);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Syncs manifest.json version with package.json
3
+ * Called by semantic-release exec plugin during release
4
+ *
5
+ * @see https://github.com/semantic-release/exec
6
+ */
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { execSync } = require("child_process");
10
+
11
+ const pkgPath = path.join(process.cwd(), "package.json");
12
+ const manifestPath = path.join(process.cwd(), "manifest.json");
13
+
14
+ if (!fs.existsSync(manifestPath)) {
15
+ console.log("[sync-manifest] No manifest.json found, skipping");
16
+ process.exit(0);
17
+ }
18
+
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
20
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
21
+
22
+ manifest.version = pkg.version;
23
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
24
+
25
+ // Run prettier to ensure consistent formatting
26
+ try {
27
+ execSync("npx prettier --write manifest.json", { stdio: "inherit" });
28
+ } catch {
29
+ console.log("[sync-manifest] prettier not available, skipping format");
30
+ }
31
+
32
+ console.log(`[sync-manifest] Updated manifest.json to ${pkg.version}`);