@macedon-technologies/batman 1.32.1 → 1.33.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.
Files changed (32) hide show
  1. package/.jsii +2 -2
  2. package/lib/BatmanProject.js +1 -1
  3. package/lib/BootcampProject.js +1 -1
  4. package/lib/CodeOwnersFile.js +1 -1
  5. package/lib/PrAutoMergeWorkflow.js +1 -1
  6. package/lib/ProductionDeployWorkflow.js +1 -1
  7. package/lib/SoftwareFactoryFiles.js +1 -1
  8. package/lib/StagingDeployWorkflow.js +1 -1
  9. package/package.json +2 -1
  10. package/templates/app/architecture-rules/README.md +97 -0
  11. package/templates/app/architecture-rules/check.ts +218 -0
  12. package/templates/app/architecture-rules/rules.json +3 -0
  13. package/templates/app/architecture-rules/tsconfig.json +14 -0
  14. package/templates/app/openspec-gates/check-tasks-complete.ts +44 -0
  15. package/templates/app/openspec-gates/tsconfig.json +13 -0
  16. package/templates/app/package.json +74 -0
  17. package/templates/app/pnpm-lock.yaml +3888 -0
  18. package/templates/app/pnpm-workspace.yaml +7 -0
  19. package/templates/app/src/aspects/DataDogAspect.ts +14 -0
  20. package/templates/app/src/constructs/Secret.ts +50 -0
  21. package/templates/app/src/constructs/handler/Secret.Function.ts +38 -0
  22. package/templates/app/src/constructs/handler/secret/client.ts +5 -0
  23. package/templates/app/src/constructs/handler/secret/createSecretValue.ts +30 -0
  24. package/templates/app/src/constructs/handler/secret/didRandomPropertiesChange.ts +13 -0
  25. package/templates/app/src/constructs/handler/secret/updateSecretValue.ts +71 -0
  26. package/templates/app/swamp-bootstrap/pull-extensions.ts +68 -0
  27. package/templates/app/swamp-bootstrap/tsconfig.json +13 -0
  28. package/templates/app/test/DataDogAspect.test.ts +31 -0
  29. package/templates/app/test/Secret.test.ts +40 -0
  30. package/templates/app/test/didRandomPropertiesChange.test.ts +19 -0
  31. package/templates/app/test/tsconfig.json +13 -0
  32. package/templates/app/tsconfig.json +50 -0
package/.jsii CHANGED
@@ -1393,6 +1393,6 @@
1393
1393
  "symbolId": "src/WorkflowActionVersions:WorkflowActionVersions"
1394
1394
  }
1395
1395
  },
1396
- "version": "1.32.1",
1397
- "fingerprint": "SA1psU4XUCoyznTMo0IHlvOXqLkoIjQxxTlCYAZMqR0="
1396
+ "version": "1.33.0",
1397
+ "fingerprint": "cGijwamH3ea9tzt5nrOJYVxPV0QTXWy0gyYBQ2wuILk="
1398
1398
  }
@@ -12,7 +12,7 @@ const SoftwareFactoryFiles_1 = require("./SoftwareFactoryFiles");
12
12
  const utils_1 = require("./utils");
13
13
  const ViteReactProject_1 = require("./ViteReactProject");
14
14
  class BatmanProject extends awscdk_1.AwsCdkTypeScriptApp {
15
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.BatmanProject", version: "1.32.1" };
15
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.BatmanProject", version: "1.33.0" };
16
16
  constructor(options) {
17
17
  super({
18
18
  ...options,
@@ -12,7 +12,7 @@ const SoftwareFactoryFiles_1 = require("./SoftwareFactoryFiles");
12
12
  const utils_1 = require("./utils");
13
13
  const ViteReactProject_1 = require("./ViteReactProject");
14
14
  class BootcampProject extends awscdk_1.AwsCdkTypeScriptApp {
15
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.BootcampProject", version: "1.32.1" };
15
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.BootcampProject", version: "1.33.0" };
16
16
  constructor(options) {
17
17
  super({
18
18
  ...options,
@@ -4,7 +4,7 @@ exports.CodeOwnersFile = void 0;
4
4
  const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
5
5
  const projen_1 = require("projen");
6
6
  class CodeOwnersFile extends projen_1.TextFile {
7
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.CodeOwnersFile", version: "1.32.1" };
7
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.CodeOwnersFile", version: "1.33.0" };
8
8
  constructor(project, options = {}) {
9
9
  const defaultOwners = ['* @macedon-technologies/platform', '* @macedon-technologies/ai-development-team'];
10
10
  const allOwners = [...defaultOwners, ...(options.additionalOwners || [])];
@@ -14,7 +14,7 @@ const PR_AUTO_MERGE_README_URL = 'https://github.com/macedon-technologies/platfo
14
14
  * workflows from pull request branches can't read the key. The job never checks out pull request code.
15
15
  */
16
16
  class PrAutoMergeWorkflow extends projen_1.YamlFile {
17
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.PrAutoMergeWorkflow", version: "1.32.1" };
17
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.PrAutoMergeWorkflow", version: "1.33.0" };
18
18
  constructor(project, options = {}) {
19
19
  const actionVersions = (0, WorkflowActionVersions_1.resolveActionVersions)(options.actionVersions);
20
20
  super(project, '.github/workflows/pr-auto-merge.yml', {
@@ -7,7 +7,7 @@ const javascript_1 = require("projen/lib/javascript");
7
7
  const utils_1 = require("./utils");
8
8
  const WorkflowActionVersions_1 = require("./WorkflowActionVersions");
9
9
  class ProductionDeployWorkflow extends projen_1.YamlFile {
10
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.ProductionDeployWorkflow", version: "1.32.1" };
10
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.ProductionDeployWorkflow", version: "1.33.0" };
11
11
  constructor(project, options) {
12
12
  const packageManager = project.package.packageManager;
13
13
  const actionVersions = (0, WorkflowActionVersions_1.resolveActionVersions)(options.actionVersions);
@@ -35,7 +35,7 @@ const DEFAULT_GITHUB_OWNER = 'macedon-technologies';
35
35
  * workflows that wire them together) is templated.
36
36
  */
37
37
  class SoftwareFactoryFiles {
38
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.SoftwareFactoryFiles", version: "1.32.1" };
38
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.SoftwareFactoryFiles", version: "1.33.0" };
39
39
  constructor(project, options) {
40
40
  const { repoName, actionVersions, attestationFiles } = options;
41
41
  const githubOwner = options.githubOwner || DEFAULT_GITHUB_OWNER;
@@ -7,7 +7,7 @@ const javascript_1 = require("projen/lib/javascript");
7
7
  const utils_1 = require("./utils");
8
8
  const WorkflowActionVersions_1 = require("./WorkflowActionVersions");
9
9
  class StagingDeployWorkflow extends projen_1.YamlFile {
10
- static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.StagingDeployWorkflow", version: "1.32.1" };
10
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "@macedon-technologies/batman.StagingDeployWorkflow", version: "1.33.0" };
11
11
  constructor(project, options) {
12
12
  const packageManager = project.package.packageManager;
13
13
  const actionVersions = (0, WorkflowActionVersions_1.resolveActionVersions)(options.actionVersions);
package/package.json CHANGED
@@ -49,7 +49,7 @@
49
49
  "main": "lib/index.js",
50
50
  "license": "Apache-2.0",
51
51
  "stability": "stable",
52
- "version": "1.32.1",
52
+ "version": "1.33.0",
53
53
  "jest": {
54
54
  "coverageProvider": "v8",
55
55
  "testMatch": [
@@ -126,6 +126,7 @@
126
126
  "release": "projen release",
127
127
  "swamp:bootstrap": "projen swamp:bootstrap",
128
128
  "test": "projen test",
129
+ "test:templates": "projen test:templates",
129
130
  "test:watch": "projen test:watch",
130
131
  "unbump": "projen unbump",
131
132
  "upgrade": "projen upgrade",
@@ -0,0 +1,97 @@
1
+ # Architecture rules
2
+
3
+ A data-driven registry of architectural constraints, enforced by
4
+ `pnpm validate:architecture` and run as part of the pre-PR verification
5
+ workflow (`workflows/workflow-pre-pr-verify.yaml`) before the swamp factory
6
+ opens a PR. New rules are new entries in `rules.json`, not new code — the
7
+ engine (`check.ts`) only needs a new case in `evaluateRule()` when a rule
8
+ needs a genuinely new kind of check.
9
+
10
+ ## Rule shape
11
+
12
+ Two `check.type`s are implemented: `forbidden-import` (whole-repo import-graph
13
+ check) and `required-companion` (diff-based co-change check).
14
+
15
+ ```json
16
+ {
17
+ "id": "unique-id",
18
+ "description": "What this enforces and why.",
19
+ "enabled": true,
20
+ "severity": "low" | "medium" | "high" | "critical",
21
+ "check": {
22
+ "type": "forbidden-import",
23
+ "from": ["glob patterns selecting files this rule inspects"],
24
+ "forbid": ["glob patterns those files may not import from"]
25
+ }
26
+ }
27
+ ```
28
+
29
+ ```json
30
+ {
31
+ "id": "unique-id",
32
+ "description": "What this enforces and why.",
33
+ "enabled": true,
34
+ "severity": "low" | "medium" | "high" | "critical",
35
+ "check": {
36
+ "type": "required-companion",
37
+ "from": ["glob patterns selecting changed files that trigger the requirement"],
38
+ "requireOneOf": ["glob patterns -- at least one CHANGED file must match one of these"],
39
+ "exempt": ["optional -- from-files matching these globs don't trigger the requirement"]
40
+ }
41
+ }
42
+ ```
43
+
44
+ `required-companion` evaluates the set of files changed in the current diff
45
+ (`git diff --name-only --diff-filter=ACMR <baseRef>...HEAD`), not the whole
46
+ repo tree: if any changed file matches `from` (and doesn't match `exempt`) but
47
+ no changed file matches `requireOneOf`, that's a violation. The base ref
48
+ defaults to `origin/main` and can be overridden with the
49
+ `ARCHITECTURE_RULES_BASE_REF` environment variable. This means the checkout
50
+ running `pnpm validate:architecture` must have that base ref fetched and
51
+ resolvable (a shallow, single-branch checkout will make the check fail closed
52
+ with an error rather than silently reporting no violations) — make sure
53
+ whatever runs this task fetches enough history for `git diff <baseRef>...HEAD`
54
+ to succeed.
55
+
56
+ `from`/`requireOneOf`/`exempt` globs are matched against the paths `git
57
+ diff --name-only` reports, which are relative to the git repository's
58
+ top-level directory. This lines up with `from`'s own directory (the parent of
59
+ `architecture-rules/`) in the common case — a single project generated at the
60
+ repo root — but if `architecture-rules/` instead lives under a nested
61
+ subdirectory of a larger git repo, write globs relative to the git root, not
62
+ to that subdirectory.
63
+
64
+ **`required-companion` is a coverage check, not a correctness check.** It only
65
+ confirms that *some* changed file matches `requireOneOf` — it can't tell
66
+ whether that change is actually related to the triggering `from` change (a
67
+ trivial or unrelated edit to a file under `requireOneOf` satisfies the rule
68
+ just as well as a substantive one), and it can't tell whether a given `from`
69
+ change genuinely needed a companion at all (a pure refactor might not).
70
+ `exempt` covers *known, glob-expressible* exceptions, but not every false
71
+ positive is expressible as a glob — that judgment call still belongs to the
72
+ rule author (scoping `from`/`exempt`) and the reviewer (reading the companion
73
+ diff, not just trusting that the check passed). Also note that `main()`
74
+ fails the whole run on *any* violation regardless of `severity` — `severity`
75
+ is informational only, not a leniency lever — so start with a narrowly
76
+ scoped `from` (or `enabled: false`) and widen it as the convention proves
77
+ itself, the same guidance as for `forbidden-import`.
78
+
79
+ `enabled: false` rules are skipped (reported as "deferred") rather than
80
+ evaluated — use this for a rule you've named and described but don't yet
81
+ have a concrete convention to check.
82
+
83
+ ## Current rules
84
+
85
+ None yet — `rules.json` ships empty. Add rules as your domain module
86
+ conventions solidify (e.g. an event-sourced/CQRS module layout under
87
+ `src/domains/<name>/{commands,events,aggregates,projections}/`).
88
+
89
+ ## Adding a check type
90
+
91
+ `forbidden-import` (whole-repo import graph) and `required-companion`
92
+ (diff-based co-change) are the implemented `check.type`s. If a future rule
93
+ needs a data source neither provides (e.g. "every aggregate file must have a
94
+ matching events file", which is a repo-wide naming convention rather than
95
+ either an import-graph or a diff check), add a new `type` value, a matching
96
+ interface, and a case in `evaluateRule()` in `check.ts` — keep the engine
97
+ itself generic; rule-specific knowledge belongs in `rules.json`.
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Generic architecture-rule engine. Rules live entirely in rules.json — this
3
+ * file only knows how to evaluate the check "types" that rules can declare.
4
+ * Add a new rule by adding a rules.json entry; add a new check type by adding
5
+ * a case in evaluateRule().
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { execFileSync } from 'node:child_process';
10
+ import rulesFile from './rules.json';
11
+
12
+ interface ForbiddenImportCheck {
13
+ type: 'forbidden-import';
14
+ // Glob patterns (relative to repo root) selecting the files this rule inspects.
15
+ from: string[];
16
+ // Glob patterns (relative to repo root) that "from" files may not import from.
17
+ forbid: string[];
18
+ }
19
+
20
+ interface RequiredCompanionCheck {
21
+ type: 'required-companion';
22
+ // Glob patterns selecting changed files that trigger the requirement.
23
+ from: string[];
24
+ // Glob patterns -- at least one CHANGED file must match one of these.
25
+ requireOneOf: string[];
26
+ // Optional -- 'from' files matching these globs don't trigger the requirement.
27
+ exempt?: string[];
28
+ }
29
+
30
+ interface Rule {
31
+ id: string;
32
+ description: string;
33
+ enabled: boolean;
34
+ severity: 'low' | 'medium' | 'high' | 'critical';
35
+ check: ForbiddenImportCheck | RequiredCompanionCheck;
36
+ }
37
+
38
+ interface Violation {
39
+ ruleId: string;
40
+ severity: string;
41
+ message: string;
42
+ }
43
+
44
+ const REPO_ROOT = path.resolve(__dirname, '..');
45
+ const IGNORED_DIRS = new Set(['node_modules', '.git', '.swamp', 'cdk.out', 'lib', 'build', 'dist']);
46
+ const BASE_REF = process.env.ARCHITECTURE_RULES_BASE_REF || 'origin/main';
47
+
48
+ // Minimal glob -> RegExp: supports "*", "**", "**/" and literal path segments.
49
+ // No new dependency for this — the only pattern shapes rules.json ever needs
50
+ // are directory-prefix globs like "src/domains/*/commands/**".
51
+ function globToRegExp(glob: string): RegExp {
52
+ let out = '';
53
+ for (let i = 0; i < glob.length; i++) {
54
+ const c = glob[i];
55
+ if (c === '*' && glob[i + 1] === '*') {
56
+ i++;
57
+ if (glob[i + 1] === '/') {
58
+ out += '(?:.*/)?';
59
+ i++;
60
+ } else {
61
+ out += '.*';
62
+ }
63
+ } else if (c === '*') {
64
+ out += '[^/]*';
65
+ } else if (c === '?') {
66
+ out += '[^/]';
67
+ } else if ('.+^${}()|[]\\'.includes(c)) {
68
+ out += `\\${c}`;
69
+ } else {
70
+ out += c;
71
+ }
72
+ }
73
+ return new RegExp(`^${out}$`);
74
+ }
75
+
76
+ function matchesAny(relFile: string, globs: string[]): boolean {
77
+ return globs.some((glob) => globToRegExp(glob).test(relFile));
78
+ }
79
+
80
+ function toRepoRelative(absPath: string): string {
81
+ return path.relative(REPO_ROOT, absPath).split(path.sep).join('/');
82
+ }
83
+
84
+ // Files changed relative to baseRef, sourced from git rather than the whole-tree
85
+ // walk() below -- required-companion checks care about what a PR touched, not
86
+ // the full repo. Throws (rather than returning []) if baseRef cannot be resolved,
87
+ // so an unfetched base ref fails the check instead of silently reporting no violations.
88
+ function getChangedFiles(baseRef: string): string[] {
89
+ let output: string;
90
+ try {
91
+ output = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACMR', `${baseRef}...HEAD`], {
92
+ cwd: REPO_ROOT,
93
+ encoding: 'utf8',
94
+ });
95
+ } catch (err) {
96
+ throw new Error(
97
+ `architecture-rules: failed to compute the changed-file set against base ref "${baseRef}" -- ` +
98
+ `is it fetched and resolvable in this checkout? (${(err as Error).message})`,
99
+ );
100
+ }
101
+ return output.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
102
+ }
103
+
104
+ function walk(dir: string, acc: string[] = []): string[] {
105
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
106
+ if (IGNORED_DIRS.has(entry.name)) continue;
107
+ const full = path.join(dir, entry.name);
108
+ if (entry.isDirectory()) {
109
+ walk(full, acc);
110
+ } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
111
+ acc.push(full);
112
+ }
113
+ }
114
+ return acc;
115
+ }
116
+
117
+ const RELATIVE_IMPORT_RE = /(?:import|export)\s+(?:[^'";]*?\bfrom\s+)?['"](\.[^'"]+)['"]/g;
118
+
119
+ function findRelativeImports(source: string): string[] {
120
+ const specs: string[] = [];
121
+ RELATIVE_IMPORT_RE.lastIndex = 0;
122
+ let match: RegExpExecArray | null;
123
+ while ((match = RELATIVE_IMPORT_RE.exec(source))) {
124
+ specs.push(match[1]);
125
+ }
126
+ return specs;
127
+ }
128
+
129
+ // An import spec resolves to a module, not a specific file extension, so a
130
+ // forbid glob is checked against the bare resolved path plus its .ts/index.ts
131
+ // variants.
132
+ function resolvedCandidates(fromFile: string, spec: string): string[] {
133
+ const resolved = toRepoRelative(path.normalize(path.join(path.dirname(fromFile), spec)));
134
+ return [resolved, `${resolved}.ts`, `${resolved}/index.ts`];
135
+ }
136
+
137
+ function evaluateForbiddenImport(rule: Rule): Violation[] {
138
+ const { from, forbid } = rule.check as ForbiddenImportCheck;
139
+ if (from.length === 0 || forbid.length === 0) return [];
140
+
141
+ const violations: Violation[] = [];
142
+ for (const file of walk(REPO_ROOT)) {
143
+ const relFile = toRepoRelative(file);
144
+ if (!matchesAny(relFile, from)) continue;
145
+
146
+ const source = fs.readFileSync(file, 'utf8');
147
+ for (const spec of findRelativeImports(source)) {
148
+ const candidates = resolvedCandidates(file, spec);
149
+ if (candidates.some((candidate) => matchesAny(candidate, forbid))) {
150
+ violations.push({
151
+ ruleId: rule.id,
152
+ severity: rule.severity,
153
+ message: `${relFile} imports "${spec}", which resolves under a path forbidden by rule "${rule.id}"`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return violations;
159
+ }
160
+
161
+ function evaluateRequiredCompanion(rule: Rule): Violation[] {
162
+ const { from, requireOneOf, exempt } = rule.check as RequiredCompanionCheck;
163
+ if (from.length === 0 || requireOneOf.length === 0) return [];
164
+
165
+ const changedFiles = getChangedFiles(BASE_REF);
166
+ const triggering = changedFiles.filter(
167
+ (file) => matchesAny(file, from) && !(exempt && matchesAny(file, exempt)),
168
+ );
169
+ if (triggering.length === 0) return [];
170
+
171
+ const satisfied = changedFiles.some((file) => matchesAny(file, requireOneOf));
172
+ if (satisfied) return [];
173
+
174
+ return [
175
+ {
176
+ ruleId: rule.id,
177
+ severity: rule.severity,
178
+ message: `changed file(s) ${triggering.join(', ')} match rule "${rule.id}"'s "from" patterns, but no changed file matches its "requireOneOf" patterns`,
179
+ },
180
+ ];
181
+ }
182
+
183
+ function evaluateRule(rule: Rule): Violation[] {
184
+ switch (rule.check.type) {
185
+ case 'forbidden-import':
186
+ return evaluateForbiddenImport(rule);
187
+ case 'required-companion':
188
+ return evaluateRequiredCompanion(rule);
189
+ default:
190
+ throw new Error(`architecture-rules: unknown check.type "${(rule.check as { type: string }).type}" on rule "${rule.id}"`);
191
+ }
192
+ }
193
+
194
+ function main(): number {
195
+ const rules = (rulesFile as { rules: Rule[] }).rules;
196
+ let evaluated = 0;
197
+ let deferred = 0;
198
+ const violations: Violation[] = [];
199
+
200
+ for (const rule of rules) {
201
+ if (!rule.enabled) {
202
+ deferred++;
203
+ console.log(`[deferred] ${rule.id}: ${rule.description}`);
204
+ continue;
205
+ }
206
+ evaluated++;
207
+ violations.push(...evaluateRule(rule));
208
+ }
209
+
210
+ console.log(`\narchitecture-rules: ${evaluated} rule(s) evaluated, ${deferred} deferred, ${violations.length} violation(s)`);
211
+ for (const violation of violations) {
212
+ console.error(` [${violation.severity}] ${violation.ruleId}: ${violation.message}`);
213
+ }
214
+
215
+ return violations.length > 0 ? 1 : 0;
216
+ }
217
+
218
+ process.exit(main());
@@ -0,0 +1,3 @@
1
+ {
2
+ "rules": []
3
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": "."
6
+ },
7
+ "include": [
8
+ "*.ts",
9
+ "*.json"
10
+ ],
11
+ "exclude": [
12
+ "node_modules"
13
+ ]
14
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gate: fails unless every task in the named OpenSpec change's tasks.md is
3
+ * checked off. `openspec validate`/`openspec status` don't check this (an
4
+ * in-progress change is a normal, valid state) — this is the deterministic
5
+ * check the pre-pr pipeline needs before letting implementation proceed.
6
+ */
7
+ import { execFileSync } from 'node:child_process';
8
+
9
+ interface OpenSpecChangeSummary {
10
+ name: string;
11
+ completedTasks: number;
12
+ totalTasks: number;
13
+ status: string;
14
+ }
15
+
16
+ function main(): number {
17
+ // pnpm's `-- <args>` forwarding (and projen's receiveArgs) can pass a
18
+ // literal "--" through as an argv token ahead of the real argument.
19
+ const slug = process.argv.slice(2).find((arg) => arg !== '--');
20
+ if (!slug) {
21
+ console.error('Usage: check-tasks-complete <change-slug>');
22
+ return 1;
23
+ }
24
+
25
+ const raw = execFileSync('openspec', ['list', '--json'], { encoding: 'utf8', shell: true });
26
+ const { changes } = JSON.parse(raw) as { changes: OpenSpecChangeSummary[] };
27
+ const change = changes.find((c) => c.name === slug);
28
+
29
+ if (!change) {
30
+ console.error(`OpenSpec change "${slug}" not found (openspec list --json)`);
31
+ return 1;
32
+ }
33
+ if (change.status !== 'complete') {
34
+ console.error(
35
+ `OpenSpec change "${slug}" has incomplete tasks: ${change.completedTasks}/${change.totalTasks}`,
36
+ );
37
+ return 1;
38
+ }
39
+
40
+ console.log(`OpenSpec change "${slug}": all ${change.totalTasks} tasks complete`);
41
+ return 0;
42
+ }
43
+
44
+ process.exit(main());
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": "."
6
+ },
7
+ "include": [
8
+ "*.ts"
9
+ ],
10
+ "exclude": [
11
+ "node_modules"
12
+ ]
13
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@macedon-technologies/batman-template-app",
3
+ "private": true,
4
+ "description": "The files Batman copies into a generated project, as a working, tested app.",
5
+ "packageManager": "pnpm@10.33.0",
6
+ "scripts": {
7
+ "test": "tsc --noEmit -p tsconfig.json && tsc -p test/tsconfig.json && tsc -p architecture-rules && tsc -p openspec-gates && tsc -p swamp-bootstrap && jest"
8
+ },
9
+ "dependencies": {
10
+ "@aws-sdk/client-secrets-manager": "*",
11
+ "@types/aws-lambda": "*",
12
+ "@types/express": "*",
13
+ "@types/pg": "*",
14
+ "aws-cdk-lib": "^2.224.0",
15
+ "constructs": "^10.5.1",
16
+ "datadog-cdk-constructs-v2": "*"
17
+ },
18
+ "devDependencies": {
19
+ "@types/jest": "*",
20
+ "@types/jsonwebtoken": "*",
21
+ "@types/node": "^22",
22
+ "esbuild": "*",
23
+ "jest": "*",
24
+ "jest-junit": "^17",
25
+ "ts-jest": "*",
26
+ "ts-node": "*",
27
+ "typescript": "*"
28
+ },
29
+ "jest": {
30
+ "coverageProvider": "v8",
31
+ "testMatch": [
32
+ "<rootDir>/@(src|test)/**/*(*.)@(spec|test).ts?(x)",
33
+ "<rootDir>/@(src|test)/**/__tests__/**/*.ts?(x)",
34
+ "<rootDir>/@(projenrc)/**/*(*.)@(spec|test).ts?(x)",
35
+ "<rootDir>/@(projenrc)/**/__tests__/**/*.ts?(x)"
36
+ ],
37
+ "clearMocks": true,
38
+ "collectCoverage": true,
39
+ "coverageReporters": [
40
+ "json",
41
+ "lcov",
42
+ "clover",
43
+ "cobertura",
44
+ "text"
45
+ ],
46
+ "coverageDirectory": "coverage",
47
+ "coveragePathIgnorePatterns": [
48
+ "/node_modules/"
49
+ ],
50
+ "testPathIgnorePatterns": [
51
+ "/node_modules/"
52
+ ],
53
+ "watchPathIgnorePatterns": [
54
+ "/node_modules/"
55
+ ],
56
+ "reporters": [
57
+ "default",
58
+ [
59
+ "jest-junit",
60
+ {
61
+ "outputDirectory": "test-reports"
62
+ }
63
+ ]
64
+ ],
65
+ "transform": {
66
+ "^.+\\.[t]sx?$": [
67
+ "ts-jest",
68
+ {
69
+ "tsconfig": "test/tsconfig.json"
70
+ }
71
+ ]
72
+ }
73
+ }
74
+ }