@eventmodelers/cli 1.0.44 → 1.0.46

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 (108) hide show
  1. package/README.md +4 -1
  2. package/cli.js +53 -1
  3. package/package.json +2 -2
  4. package/shared/build-kit/lib/checks/README.md +59 -0
  5. package/shared/build-kit/lib/ralph.js +110 -28
  6. package/shared/build-kit/lib/util/find-slice.cjs +59 -0
  7. package/shared/build-kit/ralph-claude.js +6 -2
  8. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
  9. package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
  10. package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
  11. package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  12. package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
  13. package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
  14. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
  15. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
  16. package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
  17. package/stacks/kurrent/templates/build-kit/CLAUDE.md +122 -0
  18. package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
  19. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
  20. package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
  21. package/stacks/kurrent/templates/root/README.md +46 -0
  22. package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
  23. package/stacks/kurrent/templates/root/mvnw +259 -0
  24. package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
  25. package/stacks/kurrent/templates/root/pom.xml +152 -0
  26. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  27. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
  28. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
  29. package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
  30. package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
  31. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +4 -2
  32. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
  33. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +1 -1
  34. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +2 -0
  35. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
  36. package/stacks/node/templates/build-kit/CLAUDE.md +22 -0
  37. package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
  38. package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  39. package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
  40. package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
  41. package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  42. package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  43. package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  44. package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  45. package/stacks/node/templates/root/.githooks/pre-commit +11 -0
  46. package/stacks/node/templates/root/README.md +67 -0
  47. package/stacks/node/templates/root/package.json +2 -1
  48. package/stacks/node/templates/root/setup-env.sh +7 -1
  49. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
  50. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  51. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
  52. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
  53. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
  54. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
  55. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
  56. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
  57. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +101 -0
  58. package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
  59. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
  60. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
  61. package/stacks/opencqrs/templates/root/README.md +42 -0
  62. package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
  63. package/stacks/opencqrs/templates/root/mvnw +259 -0
  64. package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
  65. package/stacks/opencqrs/templates/root/pom.xml +139 -0
  66. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  67. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
  68. package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
  69. package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
  70. package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
  71. package/stacks/supabase/templates/build-kit/CLAUDE.md +25 -0
  72. package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
  73. package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  74. package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
  75. package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
  76. package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  77. package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  78. package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  79. package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  80. package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
  81. package/stacks/supabase/templates/root/package.json +2 -1
  82. package/stacks/supabase/templates/root/setup-env.sh +7 -1
  83. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
  84. package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
  85. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
  86. package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
  87. package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
  88. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
  89. package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
  90. package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
  91. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
  92. package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
  93. package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
  94. package/stacks/umadb/templates/root/README.md +48 -0
  95. package/stacks/umadb/templates/root/docker-compose.yml +29 -0
  96. package/stacks/umadb/templates/root/mvnw +259 -0
  97. package/stacks/umadb/templates/root/mvnw.cmd +149 -0
  98. package/stacks/umadb/templates/root/pom.xml +151 -0
  99. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
  100. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
  101. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
  102. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
  103. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
  104. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
  105. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
  106. package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
  107. package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
  108. package/stacks/umadb/templates/root/src/test/resources/application.properties +8 -0
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Runner for the slice commit-scope guard. Loads every check module from
5
+ // ./checks/*.cjs and runs it against the currently staged changeset.
6
+ //
7
+ // Check interface (see ./checks/README.md for the full contract + a template):
8
+ // module.exports = {
9
+ // name: 'my-check', // short id, shown in violation output
10
+ // skipIfAlreadyFailing: false, // optional — skip this check once an earlier
11
+ // // one has already failed (use for slow checks)
12
+ // run(ctx) {
13
+ // return [{ path: 'some/file.ts', reason: 'why this is a problem' }];
14
+ // },
15
+ // };
16
+ // `run()` returns an array of violations (empty array/undefined/null = pass).
17
+ //
18
+ // `ctx` passed to every check:
19
+ // changes [{status, path}] — staged files (git status letter + path)
20
+ // touchesSlice true — this commit touches src/slices/{context}/{slice}/**
21
+ // (the runner already gates on this before loading checks)
22
+ // repoRoot absolute path to the repo root
23
+ // SLICE_PATTERN RegExp matching a path inside a slice's own folder
24
+ //
25
+ // Zero dependencies — plain Node, so it works from git's pre-commit hook
26
+ // (see ../../.githooks/pre-commit), from `npm run check:scope`, or from CI.
27
+ // Invoked as: node .build-kit/lib/check-commit-scope.cjs
28
+
29
+ const { execSync } = require('child_process');
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const SLICE_PATTERN = /^src\/slices\/[^/]+\/[^/]+\//;
34
+
35
+ function stagedChanges() {
36
+ const out = execSync('git diff --cached --name-status --no-renames', { encoding: 'utf8' });
37
+ return out
38
+ .split('\n')
39
+ .filter(Boolean)
40
+ .map((line) => {
41
+ const [status, ...rest] = line.split('\t');
42
+ return { status: status[0], path: rest.join('\t') };
43
+ });
44
+ }
45
+
46
+ function loadChecks() {
47
+ const checksDir = path.join(__dirname, 'checks');
48
+ if (!fs.existsSync(checksDir)) return [];
49
+ return fs
50
+ .readdirSync(checksDir)
51
+ .filter((f) => f.endsWith('.cjs'))
52
+ .sort() // numeric filename prefixes (00-, 10-, ...) control run order
53
+ .map((f) => {
54
+ let mod;
55
+ try {
56
+ mod = require(path.join(checksDir, f));
57
+ } catch (err) {
58
+ console.error(`check-commit-scope: failed to load checks/${f} — ${err.message}`);
59
+ return null;
60
+ }
61
+ if (typeof mod?.run !== 'function') {
62
+ console.error(`check-commit-scope: skipping checks/${f} — does not export { name, run(ctx) }`);
63
+ return null;
64
+ }
65
+ return { file: f, name: mod.name || f, run: mod.run, skipIfAlreadyFailing: !!mod.skipIfAlreadyFailing };
66
+ })
67
+ .filter(Boolean);
68
+ }
69
+
70
+ function main() {
71
+ let changes;
72
+ try {
73
+ changes = stagedChanges();
74
+ } catch (err) {
75
+ console.error('check-commit-scope: could not read staged changes —', err.message);
76
+ process.exit(1);
77
+ }
78
+
79
+ if (changes.length === 0) process.exit(0);
80
+
81
+ const touchesSlice = changes.some((c) => SLICE_PATTERN.test(c.path));
82
+ if (!touchesSlice) process.exit(0); // not a slice commit — nothing to enforce
83
+
84
+ const ctx = {
85
+ changes,
86
+ touchesSlice,
87
+ repoRoot: execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim(),
88
+ SLICE_PATTERN,
89
+ };
90
+
91
+ const checks = loadChecks();
92
+ const violations = [];
93
+ const claimedPaths = new Set(); // first check to flag a path wins — avoids repeat noise
94
+
95
+ for (const check of checks) {
96
+ if (check.skipIfAlreadyFailing && violations.length > 0) continue;
97
+
98
+ let result;
99
+ try {
100
+ result = check.run(ctx) || [];
101
+ } catch (err) {
102
+ violations.push({ path: '(check error)', reason: `[${check.name}] threw: ${err.message}` });
103
+ continue;
104
+ }
105
+
106
+ for (const v of result) {
107
+ if (claimedPaths.has(v.path)) continue;
108
+ claimedPaths.add(v.path);
109
+ violations.push({ path: v.path, reason: `[${check.name}] ${v.reason}` });
110
+ }
111
+ }
112
+
113
+ if (violations.length > 0) {
114
+ console.error('\n❌ commit blocked — slice commit-scope guard found issues:\n');
115
+ for (const v of violations) console.error(` - ${v.path} — ${v.reason}`);
116
+ console.error('\nSee .build-kit/lib/checks/ for what each check enforces.\n');
117
+ process.exit(1);
118
+ }
119
+
120
+ process.exit(0);
121
+ }
122
+
123
+ main();
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ // Rejects a slice commit that touches shared infra which must never change from
4
+ // slice work: the package manifest/lockfiles (no new/changed dependencies) and
5
+ // server.ts (routes/processors are auto-discovered — never wired there by hand).
6
+
7
+ const BLOCKED = [
8
+ {
9
+ pattern: /^(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|npm-shrinkwrap\.json)$/,
10
+ reason: 'dependency/package manifest changes are not allowed from a slice commit',
11
+ },
12
+ {
13
+ pattern: /^server\.ts$/,
14
+ reason: 'server.ts is shared infra (routes/processors are auto-discovered) — never touched by slice work',
15
+ },
16
+ ];
17
+
18
+ module.exports = {
19
+ name: 'blocked-paths',
20
+ run(ctx) {
21
+ const violations = [];
22
+ for (const { path: p } of ctx.changes) {
23
+ const hit = BLOCKED.find((b) => b.pattern.test(p));
24
+ if (hit) violations.push({ path: p, reason: hit.reason });
25
+ }
26
+ return violations;
27
+ },
28
+ };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Everything staged in a slice commit must be inside the slice's own folder, or
4
+ // one of the documented shared-infra exceptions a slice legitimately registers
5
+ // *into* (never rewrites wholesale). See build-kit/lib/AGENT.md and the
6
+ // build-state-view/build-automation SKILL.md files for what a compliant edit to
7
+ // an exception file looks like. Migration files have their own dedicated check
8
+ // (20-append-only-migrations.cjs), so they're allowed through here.
9
+
10
+ const ALLOWED_EXCEPTIONS = [
11
+ /^src\/slices\/[^/]+\/[A-Za-z0-9]+Events\.ts$/, // per-context event union (append-only)
12
+ /^src\/common\/loadPostgresEventstore\.ts$/, // projection registration / schema.migrate()
13
+ ];
14
+
15
+ const MIGRATION_PATTERN = /^migrations\/V\d+__.*\.sql$/;
16
+
17
+ module.exports = {
18
+ name: 'slice-scope',
19
+ run(ctx) {
20
+ const violations = [];
21
+ for (const { path: p } of ctx.changes) {
22
+ if (ctx.SLICE_PATTERN.test(p)) continue;
23
+ if (MIGRATION_PATTERN.test(p)) continue;
24
+ if (ALLOWED_EXCEPTIONS.some((r) => r.test(p))) continue;
25
+ violations.push({ path: p, reason: 'outside src/slices/{context}/{slice}/ and not a documented exception' });
26
+ }
27
+ return violations;
28
+ },
29
+ };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // migrations/V{n}__*.sql may only be ADDED, never modified or deleted — migrations
4
+ // are append-only; a fix belongs in a new migration, not an edit to an old one.
5
+
6
+ const MIGRATION_PATTERN = /^migrations\/V\d+__.*\.sql$/;
7
+
8
+ module.exports = {
9
+ name: 'append-only-migrations',
10
+ run(ctx) {
11
+ const violations = [];
12
+ for (const { status, path: p } of ctx.changes) {
13
+ if (!MIGRATION_PATTERN.test(p)) continue;
14
+ if (status !== 'A') {
15
+ violations.push({ path: p, reason: 'existing migrations are append-only; add a new V{n} file instead of editing this one' });
16
+ }
17
+ }
18
+ return violations;
19
+ },
20
+ };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // A slice commit that adds/changes a command handler, projection, or processor
4
+ // must also include a *.test.ts for that slice — catches the agent skipping the
5
+ // matching build skill's test step (build-state-change Step 4 / build-state-view
6
+ // Step 5 / build-automation's DeciderSpecification tests).
7
+
8
+ const { execSync } = require('child_process');
9
+
10
+ const IMPLEMENTATION_FILE = /^src\/slices\/([^/]+)\/([^/]+)\/(?:[A-Za-z0-9]+Command|[A-Za-z0-9]+Projection|processor(?:-[A-Za-z0-9]+)?)\.ts$/;
11
+
12
+ module.exports = {
13
+ name: 'test-file-present',
14
+ run(ctx) {
15
+ const sliceDirs = new Set();
16
+ for (const { path: p } of ctx.changes) {
17
+ const m = IMPLEMENTATION_FILE.exec(p);
18
+ if (m) sliceDirs.add(`src/slices/${m[1]}/${m[2]}`);
19
+ }
20
+ if (sliceDirs.size === 0) return [];
21
+
22
+ let tracked = [];
23
+ try {
24
+ tracked = execSync('git ls-files -- src/slices', { cwd: ctx.repoRoot, encoding: 'utf8' })
25
+ .split('\n')
26
+ .filter(Boolean);
27
+ } catch {
28
+ // best-effort — fall through with whatever's staged
29
+ }
30
+ const known = new Set([...tracked, ...ctx.changes.map((c) => c.path)]);
31
+
32
+ const violations = [];
33
+ for (const dir of sliceDirs) {
34
+ const hasTest = [...known].some((f) => f.startsWith(`${dir}/`) && f.endsWith('.test.ts'));
35
+ if (!hasTest) {
36
+ violations.push({
37
+ path: dir,
38
+ reason: 'no *.test.ts found for this slice — command handlers/projections/processors need test coverage',
39
+ });
40
+ }
41
+ }
42
+ return violations;
43
+ },
44
+ };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ // Heuristic: flags a field name used in a slice's Command/ReadModel type literal
4
+ // that doesn't appear anywhere in that slice's own slice.json. Not a real
5
+ // TS/schema-aware check (no parser dependency) — it regex-extracts the fields
6
+ // declared inside `Command<'Name', { ... }, ...>` and `type XReadModel = { ... }`
7
+ // literals, so unusual formatting (nested object/array field types, nonstandard
8
+ // generics) can slip past undetected. It only ever adds violations for fields it
9
+ // is confident about; when slice.json can't be found/parsed for a slice, that
10
+ // slice's files are skipped entirely rather than guessed at.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { findSliceJson, normalize } = require('../util/find-slice.cjs');
15
+
16
+ const COMMON_ALLOWED = new Set(
17
+ ['id', 'userId', 'correlationId', 'causationId', 'streamName', 'type', 'data', 'metadata', 'createdAt', 'updatedAt', 'timestamp'].map(normalize),
18
+ );
19
+
20
+ const TYPE_LITERALS = [
21
+ /(?:Command|Event)<\s*'[^']+'\s*,\s*{([^}]*)}/g,
22
+ /type\s+[A-Za-z0-9_]+ReadModel\s*=\s*{([^}]*)}/g,
23
+ ];
24
+
25
+ const FIELD_LINE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\??\s*:/gm;
26
+
27
+ function collectDeclaredFields(node, out) {
28
+ if (Array.isArray(node)) {
29
+ for (const item of node) collectDeclaredFields(item, out);
30
+ } else if (node && typeof node === 'object') {
31
+ if (typeof node.name === 'string' && ('type' in node || 'optional' in node)) {
32
+ out.add(normalize(node.name));
33
+ }
34
+ for (const key of Object.keys(node)) collectDeclaredFields(node[key], out);
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ name: 'no-invented-fields',
40
+ run(ctx) {
41
+ const bySlice = new Map(); // "context/SliceName" -> { context, sliceName, files: [] }
42
+
43
+ for (const { path: p } of ctx.changes) {
44
+ if (!p.endsWith('.ts') || p.endsWith('.test.ts')) continue;
45
+ const m = /^src\/slices\/([^/]+)\/([^/]+)\//.exec(p);
46
+ if (!m) continue;
47
+ const key = `${m[1]}/${m[2]}`;
48
+ if (!bySlice.has(key)) bySlice.set(key, { context: m[1], sliceName: m[2], files: [] });
49
+ bySlice.get(key).files.push(p);
50
+ }
51
+
52
+ const violations = [];
53
+
54
+ for (const { context, sliceName, files } of bySlice.values()) {
55
+ const slice = findSliceJson(ctx.repoRoot, context, sliceName);
56
+ if (!slice) continue; // can't verify — don't block
57
+
58
+ const declared = new Set();
59
+ collectDeclaredFields(slice, declared);
60
+ if (declared.size === 0) continue; // slice.json shape not recognized — don't block
61
+
62
+ for (const file of files) {
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(path.join(ctx.repoRoot, file), 'utf8');
66
+ } catch {
67
+ continue; // deleted/unreadable — nothing to check
68
+ }
69
+
70
+ for (const pattern of TYPE_LITERALS) {
71
+ pattern.lastIndex = 0;
72
+ let typeMatch;
73
+ while ((typeMatch = pattern.exec(content))) {
74
+ FIELD_LINE.lastIndex = 0;
75
+ let fieldMatch;
76
+ while ((fieldMatch = FIELD_LINE.exec(typeMatch[1]))) {
77
+ const raw = fieldMatch[1];
78
+ const norm = normalize(raw);
79
+ if (COMMON_ALLOWED.has(norm) || declared.has(norm)) continue;
80
+ violations.push({
81
+ path: file,
82
+ reason: `field "${raw}" is not declared anywhere in slice.json for this slice — check for an invented field`,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ return violations;
91
+ },
92
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // Heuristic: a slice's *.test.ts must have at least as many `it(...)` blocks as
4
+ // slice.json has `specifications[]` entries. Doesn't verify each spec is
5
+ // actually tested (that would need matching scenario content, not just count),
6
+ // just that nobody's silently short a test case. Skipped (not blocked) when
7
+ // slice.json can't be found or has no specifications[] array.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { findSliceJson } = require('../util/find-slice.cjs');
12
+
13
+ const TEST_FILE = /^src\/slices\/([^/]+)\/([^/]+)\/[^/]+\.test\.ts$/;
14
+ const IT_BLOCK = /\bit(?:\.(?:only|skip))?\s*\(/g;
15
+
16
+ module.exports = {
17
+ name: 'spec-coverage',
18
+ run(ctx) {
19
+ const violations = [];
20
+
21
+ for (const { path: p } of ctx.changes) {
22
+ TEST_FILE.lastIndex = 0;
23
+ const m = TEST_FILE.exec(p);
24
+ if (!m) continue;
25
+ const [, context, sliceName] = m;
26
+
27
+ const slice = findSliceJson(ctx.repoRoot, context, sliceName);
28
+ if (!slice || !Array.isArray(slice.specifications) || slice.specifications.length === 0) continue;
29
+
30
+ let content;
31
+ try {
32
+ content = fs.readFileSync(path.join(ctx.repoRoot, p), 'utf8');
33
+ } catch {
34
+ continue; // deleted — nothing to check
35
+ }
36
+
37
+ const specCount = slice.specifications.length;
38
+ const itCount = (content.match(IT_BLOCK) || []).length;
39
+
40
+ if (itCount < specCount) {
41
+ violations.push({
42
+ path: p,
43
+ reason: `slice.json declares ${specCount} specification(s) but this test file only has ${itCount} it(...) block(s)`,
44
+ });
45
+ }
46
+ }
47
+
48
+ return violations;
49
+ },
50
+ };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ // The project must still typecheck after the slice change. Uses the project's
4
+ // own `typescript` devDependency via `npx tsc --noEmit` — no new dependency,
5
+ // but it does mean this check needs node_modules already installed (same
6
+ // prerequisite `npm run build` already has).
7
+
8
+ const { execSync } = require('child_process');
9
+
10
+ module.exports = {
11
+ name: 'tsc-build',
12
+ skipIfAlreadyFailing: true, // slow — don't bother once the commit is rejected already
13
+ run(ctx) {
14
+ try {
15
+ execSync('npx tsc --noEmit', { cwd: ctx.repoRoot, stdio: 'pipe' });
16
+ return [];
17
+ } catch (err) {
18
+ const output = String(err.stdout || err.message || '').trim().split('\n').slice(0, 20).join('\n');
19
+ return [{ path: '(tsc --noEmit)', reason: `TypeScript build failed:\n${output}` }];
20
+ }
21
+ },
22
+ };
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # Installed via setup-env.sh (`git config core.hooksPath .githooks`), so this file
3
+ # is versioned and shared by every clone instead of living only in .git/hooks/.
4
+ set -e
5
+
6
+ repo_root=$(git rev-parse --show-toplevel)
7
+ script="$repo_root/.build-kit/lib/check-commit-scope.cjs"
8
+
9
+ if [ -f "$script" ]; then
10
+ node "$script"
11
+ fi
@@ -0,0 +1,67 @@
1
+ # Node.js / TypeScript Event-Sourced Service
2
+
3
+ An event-sourced backend scaffolded by [`@eventmodelers/cli`](https://www.npmjs.com/package/@eventmodelers/cli).
4
+ It uses [Emmett](https://event-driven-io.github.io/emmett/) over Postgres for the event store,
5
+ Express for the HTTP layer, and Flyway for schema migrations.
6
+
7
+ Features are built as **vertical slices** under `src/slices/`, generated from the slices on your
8
+ Eventmodelers board by the agent in `.build-kit/`.
9
+
10
+ ## Prerequisites
11
+
12
+ - Node.js 20 or later (the dev/start scripts use `node --env-file`)
13
+ - Docker and Docker Compose (for local Postgres)
14
+ - Flyway CLI on your `PATH` (for `npm run flyway:migrate`)
15
+ - [Claude Code](https://claude.com/claude-code) if you want to run the build agent
16
+
17
+ ## Getting started
18
+
19
+ 1. Start Postgres:
20
+
21
+ ```bash
22
+ docker compose up -d
23
+ ```
24
+
25
+ 2. Create your `.env`:
26
+
27
+ ```bash
28
+ cp .env.example .env
29
+ ```
30
+
31
+ Or run `./setup-env.sh` to be prompted for host, port, database, user and password.
32
+
33
+ 3. Activate the baseline migration and apply it:
34
+
35
+ ```bash
36
+ mv migrations/V1__schema.sql.example migrations/V1__schema.sql
37
+ npm install
38
+ npm run flyway:migrate
39
+ ```
40
+
41
+ `V1__schema.sql` creates the processor dead-letter queue table the runtime expects. Add your own
42
+ `V2__*.sql`, `V3__*.sql` and so on as slices introduce projections.
43
+
44
+ 4. Run the server:
45
+
46
+ ```bash
47
+ npm run build # slice routes and processors are loaded from dist/
48
+ npm run dev
49
+ ```
50
+
51
+ The API is on http://localhost:3000, with Swagger UI at http://localhost:3000/api-docs and the raw
52
+ OpenAPI document at http://localhost:3000/swagger.json.
53
+
54
+ ## Scripts
55
+
56
+ | Script | What it does |
57
+ |---|---|
58
+ | `npm run dev` | Start the server locally with `.env` loaded |
59
+ | `npm run build` | Compile TypeScript to `dist/` |
60
+ | `npm start` | Start in production mode |
61
+ | `npm test` | Run `src/**/*.test.ts` via `tsx --test` |
62
+ | `npm run flyway:migrate` | Apply pending migrations from `migrations/` |
63
+
64
+ ## Learn more
65
+
66
+ - [Eventmodelers](https://eventmodelers.ai)
67
+ - [Emmett documentation](https://event-driven-io.github.io/emmett/)
@@ -7,7 +7,8 @@
7
7
  "dev": "node --env-file=.env --require ts-node/register server.ts",
8
8
  "build": "tsc",
9
9
  "start": "NODE_ENV=production node --env-file=.env --require ts-node/register server.ts",
10
- "test": "tsx --test 'src/**/*.test.ts'"
10
+ "test": "tsx --test 'src/**/*.test.ts'",
11
+ "check:scope": "node .build-kit/lib/check-commit-scope.cjs"
11
12
  },
12
13
  "dependencies": {
13
14
  "@event-driven-io/emmett": "^0.42.1-alpha.1",
@@ -50,4 +50,10 @@ FLYWAY_PASSWORD=${DB_PASSWORD}
50
50
  EOF
51
51
 
52
52
  echo ""
53
- echo ".env created successfully."
53
+ echo ".env created successfully."
54
+
55
+ if [ -f .githooks/pre-commit ] && git rev-parse --git-dir >/dev/null 2>&1; then
56
+ chmod +x .githooks/pre-commit 2>/dev/null || true
57
+ git config core.hooksPath .githooks
58
+ echo "Configured git to use .githooks/ (slice commit-scope guard)."
59
+ fi