@mettlecast/domain-cli 0.2.0 → 0.2.2

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 (63) hide show
  1. package/dist/builder/build-registry.js +3 -0
  2. package/dist/builder/load-module.js +5 -2
  3. package/dist/commands/create-project.js +1 -1
  4. package/dist/commands/doctor.js +10 -10
  5. package/dist/commands/upgrade-frontend.js +0 -2
  6. package/dist/commands/upgrade.js +4 -0
  7. package/dist/templates/patterns/api/create-with-event.js +3 -7
  8. package/dist/templates/patterns/api/idempotent-mutation.js +17 -40
  9. package/dist/templates/patterns/api/paginated-list.js +1 -9
  10. package/dist/templates/patterns/api/simple-crud.js +2 -2
  11. package/dist/templates/patterns/api/system-admin.js +1 -4
  12. package/dist/templates/patterns/api/webhook-receiver-style.js +7 -53
  13. package/dist/templates/patterns/subscriber/audit-relay.js +14 -24
  14. package/dist/templates/patterns/subscriber/cascade-deletion.js +6 -22
  15. package/dist/templates/patterns/subscriber/single-step-projection.js +6 -10
  16. package/dist/templates/subscriber-skeleton.js +1 -1
  17. package/dist/utils/install-file.d.ts +5 -6
  18. package/dist/utils/install-file.js +9 -25
  19. package/dist/utils/manifest.d.ts +2 -2
  20. package/dist/utils/manifest.js +47 -5
  21. package/dist/utils/scaffold-config.d.ts +7 -0
  22. package/dist/utils/scaffold-config.js +3 -0
  23. package/package.json +1 -1
  24. package/src/__tests__/commands/upgrade.test.ts +8 -8
  25. package/src/__tests__/doctor.test.ts +16 -11
  26. package/src/__tests__/scaffold-src/part-a-layout.test.ts +0 -8
  27. package/src/__tests__/scripts/package-scaffold.test.ts +14 -14
  28. package/src/__tests__/utils/install-file.test.ts +58 -35
  29. package/src/__tests__/utils/manifest.test.ts +74 -29
  30. package/src/builder/build-registry.ts +2 -0
  31. package/src/builder/load-module.ts +5 -3
  32. package/src/commands/add-module.ts +1 -1
  33. package/src/commands/create-project.ts +2 -2
  34. package/src/commands/doctor.ts +11 -11
  35. package/src/commands/upgrade-frontend.ts +0 -2
  36. package/src/commands/upgrade.ts +7 -2
  37. package/src/templates/dashboard-pages/account/api-keys.tsx +1 -1
  38. package/src/templates/dashboard-pages/account/audit-log.tsx +1 -1
  39. package/src/templates/dashboard-pages/account/members.tsx +1 -1
  40. package/src/templates/dashboard-pages/account/profile.tsx +1 -1
  41. package/src/templates/dashboard-pages/account/workspace-settings.tsx +1 -1
  42. package/src/templates/dashboard-pages/auth/accept-invitation.tsx +1 -1
  43. package/src/templates/dashboard-pages/auth/choose-org.tsx +1 -1
  44. package/src/templates/dashboard-pages/auth/forgot-password.tsx +1 -1
  45. package/src/templates/dashboard-pages/auth/login.tsx +1 -1
  46. package/src/templates/dashboard-pages/auth/mfa-setup.tsx +1 -1
  47. package/src/templates/dashboard-pages/auth/mfa-verify.tsx +1 -1
  48. package/src/templates/dashboard-pages/auth/reset-password.tsx +1 -1
  49. package/src/templates/dashboard-pages/auth/signup.tsx +1 -1
  50. package/src/templates/dashboard-pages/auth/verify-email.tsx +1 -1
  51. package/src/templates/patterns/api/create-with-event.ts +3 -7
  52. package/src/templates/patterns/api/idempotent-mutation.ts +17 -40
  53. package/src/templates/patterns/api/paginated-list.ts +1 -9
  54. package/src/templates/patterns/api/simple-crud.ts +2 -2
  55. package/src/templates/patterns/api/system-admin.ts +1 -4
  56. package/src/templates/patterns/api/webhook-receiver-style.ts +7 -53
  57. package/src/templates/patterns/subscriber/audit-relay.ts +14 -24
  58. package/src/templates/patterns/subscriber/cascade-deletion.ts +6 -22
  59. package/src/templates/patterns/subscriber/single-step-projection.ts +6 -10
  60. package/src/templates/subscriber-skeleton.ts +1 -1
  61. package/src/utils/install-file.ts +11 -29
  62. package/src/utils/manifest.ts +51 -6
  63. package/src/utils/scaffold-config.ts +11 -0
@@ -42,6 +42,9 @@ export async function buildRegistry(domainRoot) {
42
42
  const domainExports = await load(paths.domain);
43
43
  const domainRaw = domainExports.find(e => e['_kind'] === 'domain');
44
44
  if (!domainRaw) {
45
+ // Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
46
+ for (const w of warnings)
47
+ process.stderr.write(`[tib validate] ${w}\n`);
45
48
  throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
46
49
  }
47
50
  const domain = {
@@ -87,12 +87,15 @@ export async function loadModuleExports(absoluteFilePath) {
87
87
  return await new Promise((resolve, reject) => {
88
88
  const tsxChild = spawn(process.execPath, [tsxCli, tempPath], {
89
89
  stdio: ['ignore', 'pipe', 'pipe'],
90
- env: { ...process.env },
91
90
  });
92
91
  let stdout = '';
93
92
  let stderr = '';
94
93
  tsxChild.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
95
- tsxChild.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
94
+ // Pipe tsx stderr directly to our process stderr so errors are visible in CI logs.
95
+ tsxChild.stderr.on('data', (chunk) => {
96
+ stderr += chunk.toString();
97
+ process.stderr.write(chunk);
98
+ });
96
99
  tsxChild.on('close', (code) => {
97
100
  if (code !== 0) {
98
101
  reject(new Error(`loadModuleExports: tsx exited ${code} for ${absoluteFilePath}\n${stderr}`));
@@ -231,7 +231,7 @@ export async function runCreateProject(opts) {
231
231
  sha256,
232
232
  wasTemplate: fileEntry.isTemplate,
233
233
  installedAt: new Date().toISOString(),
234
- policy: fileEntry.policy ?? 'tracked',
234
+ policy: fileEntry.policy ?? 'editable',
235
235
  });
236
236
  }
237
237
  }
@@ -1028,7 +1028,7 @@ async function checkLayoutPolicy(projectRoot) {
1028
1028
  }
1029
1029
  // Check 1: owned files in unexpected locations
1030
1030
  // Policy: owned files should be in .mc/ OR .github/workflows/tib-*
1031
- const ownedOutsideTib = manifest.files.filter(f => f.policy === 'owned' &&
1031
+ const ownedOutsideTib = manifest.files.filter(f => f.policy === 'managed' &&
1032
1032
  !f.path.startsWith('.mc/') &&
1033
1033
  !/^\.github\/workflows\/tib-/.test(f.path));
1034
1034
  if (ownedOutsideTib.length === 0) {
@@ -1050,7 +1050,7 @@ async function checkLayoutPolicy(projectRoot) {
1050
1050
  }
1051
1051
  }
1052
1052
  // Check 2: tracked/seed files inside .mc/
1053
- const nonOwnedInsideTib = manifest.files.filter(f => (f.policy === 'tracked' || f.policy === 'seed') &&
1053
+ const nonOwnedInsideTib = manifest.files.filter(f => (f.policy === 'editable' || f.policy === 'seed') &&
1054
1054
  f.path.startsWith('.mc/'));
1055
1055
  if (nonOwnedInsideTib.length === 0) {
1056
1056
  results.push({
@@ -1157,14 +1157,14 @@ async function checkLayoutPolicy(projectRoot) {
1157
1157
  * Old path → new path mapping for known scaffold files
1158
1158
  */
1159
1159
  const RELOCATION_MAP = [
1160
- { oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'owned' },
1161
- { oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'owned' },
1162
- { oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'owned' },
1163
- { oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'owned' },
1164
- { oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'owned' },
1165
- { oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'owned' },
1166
- { oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'owned' },
1167
- { oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'owned' },
1160
+ { oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'managed' },
1161
+ { oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'managed' },
1162
+ { oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'managed' },
1163
+ { oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
1164
+ { oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
1165
+ { oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
1166
+ { oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
1167
+ { oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
1168
1168
  ];
1169
1169
  /**
1170
1170
  * Run relocation of old-layout scaffold files to new layout
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import { cliLogger } from '../utils/logger.js';
4
4
  const TIB_PACKAGES = [
5
- '@mettlecast/design-system',
6
5
  '@mettlecast/dashboard-shell',
7
6
  '@mettlecast/sdk',
8
7
  '@mettlecast/observability',
@@ -10,7 +9,6 @@ const TIB_PACKAGES = [
10
9
  '@mettlecast/tsconfig-react',
11
10
  ];
12
11
  const REQUIRED_PACKAGES = [
13
- '@mettlecast/design-system',
14
12
  '@mettlecast/dashboard-shell',
15
13
  '@mettlecast/sdk',
16
14
  ];
@@ -369,6 +369,10 @@ export async function runUpgrade(packageSpec, opts) {
369
369
  }
370
370
  // Add to results
371
371
  allResults.push({ path: installPath, status: installResult.status, module: mod.id });
372
+ // update-available: do NOT update manifest (file was not written to disk)
373
+ if (installResult.status === 'update-available') {
374
+ continue;
375
+ }
372
376
  // Update manifest if file was not skipped
373
377
  const newChecksum = computeChecksumString(newContent);
374
378
  upsertManifestFile(updatedManifest, {
@@ -38,7 +38,7 @@ export const ${varName} = defineApi({
38
38
  const entityId = crypto.randomUUID();
39
39
 
40
40
  // ── Persist entity ────────────────────────────────────
41
- // ctx.db.put({ ... })
41
+ await ctx.store.put(entityId, { id: entityId, name: input.name });
42
42
 
43
43
  // ── Publish domain event ──────────────────────────────
44
44
  await ctx.publish(
@@ -46,16 +46,12 @@ export const ${varName} = defineApi({
46
46
  {
47
47
  id: entityId,
48
48
  name: input.name,
49
- tenantId: ctx.tenantId,
49
+ tenantId: ctx.tenant.id,
50
50
  },
51
51
  1,
52
52
  );
53
53
 
54
- await ctx.auditLog({
55
- action: '${id}.create',
56
- tenantId: ctx.tenantId,
57
- entityId,
58
- });
54
+ await ctx.audit.log('${id}.create', entityId, { tenantId: ctx.tenant.id });
59
55
 
60
56
  return {
61
57
  id: entityId,
@@ -11,10 +11,6 @@ export function idempotentMutationTemplate(domain, id, tenancy) {
11
11
  return `import { z } from 'zod';
12
12
  import { defineApi } from '@mettlecast/domain-runtime';
13
13
 
14
- // ── Idempotency key header ───────────────────────────────────
15
-
16
- const IdempotencyKey = z.string().min(1);
17
-
18
14
  // ── Zod schemas ──────────────────────────────────────────────
19
15
 
20
16
  const ${varName}Input = z.object({
@@ -40,56 +36,37 @@ export const ${varName} = defineApi({
40
36
  input: ${varName}Input,
41
37
  output: ${varName}Output,
42
38
  handler: async (input, ctx) => {
43
- // ── Extract idempotency key from headers ──────────────
44
- const idempotencyKey = ctx.headers?.['x-idempotency-key'];
45
- if (!idempotencyKey) {
46
- throw new Error('Missing x-idempotency-key header');
47
- }
48
- IdempotencyKey.parse(idempotencyKey);
49
-
50
- // ── Check for duplicate ───────────────────────────────
51
- const existing = await ctx.db.get({
52
- key: { pk: 'IDEMPOTENCY', sk: idempotencyKey },
53
- });
39
+ // ── Check for duplicate (idempotency key = input.id) ──
40
+ const existing = await ctx.store.get('IDEMPOTENT#' + input.id);
54
41
 
55
42
  if (existing) {
56
- await ctx.auditLog({
57
- action: '${id}.idempotent.duplicate',
58
- tenantId: ctx.tenantId,
59
- meta: { idempotencyKey },
60
- });
43
+ await ctx.audit.log('${id}.idempotent.duplicate', input.id);
61
44
  return {
62
- id: existing.entityId as string,
45
+ id: input.id,
63
46
  status: 'already-processed',
64
- idempotencyKey,
47
+ idempotencyKey: input.id,
65
48
  };
66
49
  }
67
50
 
68
51
  // ── Apply mutation ────────────────────────────────────
69
- const result = await ctx.db.put({
70
- pk: '${id.toUpperCase()}',
71
- sk: input.id,
72
- payload: input.payload,
73
- });
52
+ await ctx.store.put(
53
+ '${id.toUpperCase()}#' + input.id,
54
+ { id: input.id, payload: input.payload, createdAt: new Date().toISOString() },
55
+ );
74
56
 
75
- // ── Record idempotency token ──────────────────────────
76
- await ctx.db.put({
77
- key: { pk: 'IDEMPOTENCY', sk: idempotencyKey },
78
- entityId: input.id,
79
- ttl: Math.floor(Date.now() / 1000) + 86400, // 24h TTL
80
- });
57
+ // ── Record idempotency token with 24h TTL ─────────────
58
+ await ctx.store.put(
59
+ 'IDEMPOTENT#' + input.id,
60
+ { entityId: input.id },
61
+ { ttl: Math.floor(Date.now() / 1000) + 86400 },
62
+ );
81
63
 
82
- await ctx.auditLog({
83
- action: '${id}.idempotent.applied',
84
- tenantId: ctx.tenantId,
85
- entityId: input.id,
86
- meta: { idempotencyKey },
87
- });
64
+ await ctx.audit.log('${id}.idempotent.applied', input.id, { tenantId: ctx.tenant.id });
88
65
 
89
66
  return {
90
67
  id: input.id,
91
68
  status: 'applied',
92
- idempotencyKey,
69
+ idempotencyKey: input.id,
93
70
  };
94
71
  },
95
72
  },
@@ -44,15 +44,7 @@ export const ${varName} = defineApi({
44
44
  handler: async (input, ctx) => {
45
45
  const { cursor, limit, filter } = input;
46
46
 
47
- // Emit EMF (Embedded Metric Format) metrics
48
- ctx.metrics?.putMetric('${id}.pageRequest', 1, 'Count');
49
- ctx.metrics?.putMetric('${id}.pageSize', limit, 'Count');
50
-
51
- await ctx.auditLog({
52
- action: '${id}.list',
53
- tenantId: ctx.tenantId,
54
- meta: { cursor, limit },
55
- });
47
+ await ctx.audit.log('${id}.list', ctx.tenant.id, { cursor, limit });
56
48
 
57
49
  // ── paginated data fetch ──────────────────────────────
58
50
  const items: Array<{ id: string }> = [];
@@ -41,7 +41,7 @@ export const ${varName} = defineApi({
41
41
  output: ${varName}Output,
42
42
  handler: async (input, ctx) => {
43
43
  // Create / Read / Update / Delete based on method
44
- await ctx.auditLog({ action: '${id}.v1', tenantId: ctx.tenantId, input });
44
+ await ctx.audit.log('${id}.v1', ctx.tenant.id, { input });
45
45
  return {
46
46
  id: input.id ?? crypto.randomUUID(),
47
47
  createdAt: new Date().toISOString(),
@@ -85,7 +85,7 @@ export const ${varName}List = defineApi({
85
85
  input: ${varName}ListInput,
86
86
  output: ${varName}ListOutput,
87
87
  handler: async (input, ctx) => {
88
- await ctx.auditLog({ action: '${id}-list.v1', tenantId: ctx.tenantId });
88
+ await ctx.audit.log('${id}-list.v1', ctx.tenant.id);
89
89
  return { items: [], nextCursor: undefined };
90
90
  },
91
91
  },
@@ -45,10 +45,7 @@ export const ${varName} = defineApi({
45
45
  input: ${varName}Input,
46
46
  output: ${varName}Output,
47
47
  handler: async (input, ctx) => {
48
- await ctx.auditLog({
49
- action: '${id}.system',
50
- meta: { command: input.command },
51
- });
48
+ await ctx.audit.log('${id}.system', 'system', { command: input.command });
52
49
 
53
50
  switch (input.command) {
54
51
  case 'status':
@@ -9,7 +9,6 @@ export function webhookReceiverStyleTemplate(domain, id, tenancy) {
9
9
  const varName = camelCase(id);
10
10
  return `import { z } from 'zod';
11
11
  import { defineApi } from '@mettlecast/domain-runtime';
12
- import crypto from 'crypto';
13
12
 
14
13
  // ── Supported webhook providers ───────────────────────────────
15
14
 
@@ -35,40 +34,6 @@ const ${varName}Output = z.object({
35
34
  provider: ProviderEnum,
36
35
  });
37
36
 
38
- // ── Signature verification ────────────────────────────────────
39
-
40
- function verifySignature(
41
- provider: z.infer<typeof ProviderEnum>,
42
- payload: string,
43
- signatureHeader: string,
44
- secret: string,
45
- ): boolean {
46
- switch (provider) {
47
- case 'stripe':
48
- // Stripe uses HMAC-SHA256
49
- return crypto.timingSafeEqual(
50
- Buffer.from(signatureHeader),
51
- crypto.createHmac('sha256', secret).update(payload).digest(),
52
- );
53
- case 'github':
54
- // GitHub uses HMAC-SHA256 with 'sha256=' prefix
55
- const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
56
- return crypto.timingSafeEqual(
57
- Buffer.from(signatureHeader),
58
- Buffer.from(expected),
59
- );
60
- case 'slack':
61
- // Slack uses HMAC-SHA256 with 'v0=' prefix
62
- const slackExpected = 'v0=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
63
- return crypto.timingSafeEqual(
64
- Buffer.from(signatureHeader),
65
- Buffer.from(slackExpected),
66
- );
67
- default:
68
- return true; // custom provider — verify externally
69
- }
70
- }
71
-
72
37
  // ── API definition ───────────────────────────────────────────
73
38
 
74
39
  export const ${varName} = defineApi({
@@ -81,19 +46,8 @@ export const ${varName} = defineApi({
81
46
  input: ${varName}Input,
82
47
  output: ${varName}Output,
83
48
  handler: async (input, ctx) => {
84
- // ── Retrieve webhook secret ───────────────────────────
85
- const secret = await ctx.secrets.get(
86
- '${domain.toUpperCase()}_WEBHOOK_SECRET',
87
- { provider: input.provider },
88
- );
89
-
90
- // ── Verify signature ──────────────────────────────────
91
- const signatureHeader = ctx.headers?.['x-webhook-signature'] ?? '';
92
- const rawPayload = input.rawBody ?? JSON.stringify(input.payload);
93
-
94
- if (!verifySignature(input.provider, rawPayload, signatureHeader, secret)) {
95
- throw new Error('Invalid webhook signature');
96
- }
49
+ // Signature verification belongs in a Lambda authorizer (x-webhook-signature header
50
+ // is not accessible here). Wire an HttpLambdaAuthorizer in DomainStack for production.
97
51
 
98
52
  const eventId = crypto.randomUUID();
99
53
 
@@ -103,13 +57,13 @@ export const ${varName} = defineApi({
103
57
  provider: input.provider,
104
58
  eventType: input.eventType,
105
59
  payload: input.payload,
106
- tenantId: ctx.tenantId,
60
+ tenantId: ctx.tenant.id,
107
61
  }, 1);
108
62
 
109
- await ctx.auditLog({
110
- action: '${id}.webhook.received',
111
- tenantId: ctx.tenantId,
112
- meta: { provider: input.provider, eventType: input.eventType, eventId },
63
+ await ctx.audit.log('${id}.webhook.received', ctx.tenant.id, {
64
+ provider: input.provider,
65
+ eventType: input.eventType,
66
+ eventId,
113
67
  });
114
68
 
115
69
  return {
@@ -20,31 +20,21 @@ export const ${varName} = defineSubscriber({
20
20
  event: '${event}',
21
21
  semverRange: '^1',
22
22
  handler: async (event, ctx) => {
23
- const auditRecord = {
24
- pk: 'AUDIT#' + event.data.tenantId,
25
- sk: 'EVENT#' + event.id,
26
- eventType: event.type,
27
- eventVersion: event.version,
28
- payload: event.data,
29
- timestamp: event.timestamp,
30
- source: event.source,
31
- correlationId: event.correlationId,
32
- ttl: Math.floor(Date.now() / 1000) + 90 * 86400, // 90-day retention
33
- };
23
+ const ev = event as { id?: string; type?: string; version?: number; data?: unknown; timestamp?: string };
24
+ const eventId = ev.id ?? crypto.randomUUID();
34
25
 
35
- // ── Write to audit store ─────────────────────────────────
36
- await ctx.db.put(auditRecord);
37
-
38
- // ── Optionally forward to external sink ──────────────────
39
- if (ctx.integrations?.auditSink) {
40
- await ctx.fetch(ctx.integrations.auditSink, {
41
- method: 'POST',
42
- body: JSON.stringify(auditRecord),
43
- headers: { 'content-type': 'application/json' },
44
- });
45
- }
46
-
47
- ctx.metrics?.putMetric('${id}.audit.relayed', 1, 'Count');
26
+ // ── Write immutable audit record ─────────────────────────
27
+ await ctx.store.put(
28
+ 'AUDIT#' + eventId,
29
+ {
30
+ eventId,
31
+ eventType: ev.type,
32
+ eventVersion: ev.version,
33
+ payload: ev.data,
34
+ timestamp: ev.timestamp ?? new Date().toISOString(),
35
+ },
36
+ { ttl: Math.floor(Date.now() / 1000) + 90 * 86400 }, // 90-day retention
37
+ );
48
38
  },
49
39
  });
50
40
  `;
@@ -22,38 +22,22 @@ export const ${varName} = defineSubscriber({
22
22
  event: '${event}',
23
23
  semverRange: '^1',
24
24
  handler: async (event, ctx) => {
25
- const { entityId, tenantId } = event.data;
25
+ const data = (event as { data: { entityId: string; deletedBy?: string } }).data;
26
26
 
27
27
  // ── Find all children ────────────────────────────────────
28
- const children = await ctx.db.query({
29
- pk: 'TENANT#' + tenantId,
30
- skPrefix: 'CHILD#' + entityId,
31
- });
28
+ const { items: children } = await ctx.store.query({ skPrefix: 'CHILD#' + data.entityId });
32
29
 
33
30
  // ── Soft-delete each child ───────────────────────────────
34
31
  const now = new Date().toISOString();
35
32
  for (const child of children) {
36
- await ctx.db.update({
37
- key: { pk: child.pk, sk: child.sk },
38
- updates: {
39
- deletedAt: now,
40
- deletedBy: event.data.deletedBy ?? 'system',
41
- },
33
+ await ctx.store.update(child['sk'] as string, {
34
+ deletedAt: now,
35
+ deletedBy: data.deletedBy ?? 'system',
42
36
  });
43
37
  }
44
38
 
45
39
  // ── Audit log ────────────────────────────────────────────
46
- await ctx.auditLog({
47
- action: 'cascade.delete',
48
- tenantId,
49
- entityId,
50
- meta: {
51
- childrenDeleted: children.length,
52
- triggeredBy: event.id,
53
- },
54
- });
55
-
56
- ctx.metrics?.putMetric('${id}.cascade.deleted', children.length, 'Count');
40
+ await ctx.audit.log('cascade.delete', data.entityId, { childrenDeleted: children.length });
57
41
  },
58
42
  });
59
43
  `;
@@ -17,19 +17,15 @@ export const ${varName} = defineSubscriber({
17
17
  event: '${event}',
18
18
  semverRange: '^1',
19
19
  handler: async (event, ctx) => {
20
- const { tenantId, ...eventData } = event.data;
20
+ const { tenantId: _tenantId, ...eventData } = (event as { data: Record<string, unknown> }).data;
21
+ const eventId = (event as { id?: string }).id ?? 'unknown';
21
22
 
22
23
  // ── Upsert projection ────────────────────────────────────
23
24
  // Each event updates the projection row for fast reads.
24
- await ctx.db.put({
25
- pk: 'TENANT#' + tenantId,
26
- sk: 'PROJECTION#' + event.id,
27
- ...eventData,
28
- lastEventId: event.id,
29
- projectedAt: new Date().toISOString(),
30
- });
31
-
32
- ctx.metrics?.putMetric('${id}.projection.applied', 1, 'Count');
25
+ await ctx.store.put(
26
+ 'PROJECTION#' + eventId,
27
+ { ...eventData, lastEventId: eventId, projectedAt: new Date().toISOString() },
28
+ );
33
29
  },
34
30
  });
35
31
  `;
@@ -16,7 +16,7 @@ export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = d
16
16
  event: '${eventId}',
17
17
  semverRange: '*',
18
18
  handler: async (_event, _ctx) => {
19
- // TODO: Implement subscriber handler
19
+ // receive _event.data (typed payload) and use _ctx.store, _ctx.publish, _ctx.audit
20
20
  },
21
21
  });
22
22
  `;
@@ -1,15 +1,14 @@
1
1
  import type { ManifestFileEntry } from './manifest.js';
2
- export type FilePolicy = 'owned' | 'tracked' | 'seed';
2
+ export type FilePolicy = 'managed' | 'editable' | 'seed';
3
3
  export interface InstallResult {
4
- status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged';
4
+ status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged' | 'update-available';
5
5
  }
6
6
  /**
7
7
  * Install a scaffold file according to its policy.
8
8
  *
9
- * - owned: always overwrite. Return 'added' if new, 'updated' if existed.
10
- * - tracked: check sha256. If same → 'unchanged'. If different, check disk for drift.
11
- * If drifted write .tib-upgrade file 'conflict'. If not drifted overwrite → 'updated'.
12
- * If no currentEntry → write new → 'added'.
9
+ * - managed: always overwrite. Return 'added' if new, 'updated' if existed.
10
+ * - editable: check sha256. If same → 'unchanged'. If different 'update-available'
11
+ * (does not write .tib-upgrade or overwrite user opts in via Updates tab).
13
12
  * - seed: if file exists → 'skipped'. Else write → 'added'.
14
13
  */
15
14
  export declare function installScaffoldFile(absPath: string, content: string, policy: FilePolicy, currentEntry: ManifestFileEntry | undefined, opts?: {
@@ -1,13 +1,12 @@
1
1
  import { writeFile, mkdir, access } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
- import { computeChecksumString, computeChecksumFile } from './checksum.js';
3
+ import { computeChecksumString } from './checksum.js';
4
4
  /**
5
5
  * Install a scaffold file according to its policy.
6
6
  *
7
- * - owned: always overwrite. Return 'added' if new, 'updated' if existed.
8
- * - tracked: check sha256. If same → 'unchanged'. If different, check disk for drift.
9
- * If drifted write .tib-upgrade file 'conflict'. If not drifted overwrite → 'updated'.
10
- * If no currentEntry → write new → 'added'.
7
+ * - managed: always overwrite. Return 'added' if new, 'updated' if existed.
8
+ * - editable: check sha256. If same → 'unchanged'. If different 'update-available'
9
+ * (does not write .tib-upgrade or overwrite user opts in via Updates tab).
11
10
  * - seed: if file exists → 'skipped'. Else write → 'added'.
12
11
  */
13
12
  export async function installScaffoldFile(absPath, content, policy, currentEntry, opts) {
@@ -23,8 +22,8 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
23
22
  return false;
24
23
  }
25
24
  };
26
- if (policy === 'owned') {
27
- // owned: always overwrite, no sha256 check
25
+ if (policy === 'managed') {
26
+ // managed: always overwrite, no sha256 check
28
27
  const exists = await fileExists(absPath);
29
28
  if (!dryRun) {
30
29
  await mkdir(dirname(absPath), { recursive: true });
@@ -44,7 +43,7 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
44
43
  }
45
44
  return { status: 'added' };
46
45
  }
47
- // policy === 'tracked'
46
+ // policy === 'editable'
48
47
  if (!currentEntry) {
49
48
  // New file: write it
50
49
  if (!dryRun) {
@@ -58,21 +57,6 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
58
57
  // Content unchanged
59
58
  return { status: 'unchanged' };
60
59
  }
61
- // Content changed: check for drift
62
- const diskChecksum = await computeChecksumFile(absPath);
63
- const drifted = diskChecksum !== null && diskChecksum !== currentEntry.sha256;
64
- if (drifted) {
65
- // Drift detected: write .tib-upgrade file, don't touch original
66
- if (!dryRun) {
67
- await mkdir(dirname(absPath), { recursive: true });
68
- await writeFile(`${absPath}.tib-upgrade`, content, 'utf-8');
69
- }
70
- return { status: 'conflict' };
71
- }
72
- // No drift: safe to overwrite
73
- if (!dryRun) {
74
- await mkdir(dirname(absPath), { recursive: true });
75
- await writeFile(absPath, content, 'utf-8');
76
- }
77
- return { status: 'updated' };
60
+ // Content changed: report update available, do NOT modify disk
61
+ return { status: 'update-available' };
78
62
  }
@@ -1,4 +1,4 @@
1
- export type FilePolicy = 'owned' | 'tracked' | 'seed';
1
+ export type FilePolicy = 'managed' | 'editable' | 'seed';
2
2
  export interface ManifestFileEntry {
3
3
  path: string;
4
4
  module: string;
@@ -25,4 +25,4 @@ export declare function createManifest(scaffoldVersion: string, projectName: str
25
25
  export declare function upsertManifestFile(manifest: TibManifest, entry: ManifestFileEntry): void;
26
26
  export declare function removeManifestFile(manifest: TibManifest, filePath: string): void;
27
27
  export declare function getManifestFile(manifest: TibManifest, filePath: string): ManifestFileEntry | undefined;
28
- export declare function isScaffoldOwned(manifest: TibManifest, filePath: string): boolean;
28
+ export declare function isScaffoldManaged(manifest: TibManifest, filePath: string): boolean;