@elevasis/core 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,194 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { parse as parseYaml } from 'yaml'
4
+ import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
5
+
6
+ export { ScaffoldRegistrySchema, ScaffoldEntryKindSchema } from './schema'
7
+ export type { ScaffoldRegistry, ScaffoldRegistryEntry, ScaffoldRef, ScaffoldEntryKind } from './schema'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Paths (resolved relative to the monorepo root, not this file's location)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Resolve a path relative to the monorepo root.
15
+ * Works whether this module is running from packages/core/src or from dist/.
16
+ */
17
+ function monorepoRoot(): string {
18
+ // Walk up from __dirname until we find the .claude directory (monorepo marker)
19
+ const { dirname } = path
20
+ let dir = __dirname
21
+ for (let i = 0; i < 8; i++) {
22
+ try {
23
+ readFileSync(path.join(dir, '.claude', 'settings.json'))
24
+ return dir
25
+ } catch {
26
+ dir = dirname(dir)
27
+ }
28
+ }
29
+ throw new Error(
30
+ 'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
31
+ )
32
+ }
33
+
34
+ const YAML_FILENAME = '.claude/scaffold-registry.yml'
35
+ const JSON_FILENAME = '.claude/scaffold-registry.compiled.json'
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Load + validate
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Load and Zod-validate the scaffold registry from `.claude/scaffold-registry.yml`.
43
+ *
44
+ * Throws if:
45
+ * - The YAML file is missing or unreadable
46
+ * - The YAML fails Zod validation
47
+ * - The compiled JSON is present but its `entries` count differs from the YAML
48
+ * (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
49
+ */
50
+ export function loadScaffoldRegistry(): ScaffoldRegistry {
51
+ const root = monorepoRoot()
52
+ const yamlPath = path.join(root, YAML_FILENAME)
53
+
54
+ let raw: string
55
+ try {
56
+ raw = readFileSync(yamlPath, 'utf-8')
57
+ } catch (err) {
58
+ throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
59
+ }
60
+
61
+ const parsed = parseYaml(raw) as unknown
62
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
63
+
64
+ if (!result.success) {
65
+ const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
66
+ throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
67
+ }
68
+
69
+ const registry = result.data
70
+
71
+ // Drift check: if compiled JSON exists, verify entry count matches
72
+ const jsonPath = path.join(root, JSON_FILENAME)
73
+ try {
74
+ const compiledRaw = readFileSync(jsonPath, 'utf-8')
75
+ const compiled = JSON.parse(compiledRaw) as { entries?: unknown[] }
76
+ if (compiled.entries?.length !== registry.entries.length) {
77
+ throw new Error(
78
+ `scaffold-registry: compiled JSON is out of sync with YAML ` +
79
+ `(YAML has ${registry.entries.length} entries, JSON has ${compiled.entries?.length ?? 0}). ` +
80
+ `Run compileScaffoldRegistry() to regenerate.`
81
+ )
82
+ }
83
+ } catch (err) {
84
+ // If the file doesn't exist, skip drift check silently (first-run scenario)
85
+ if ((err as { code?: string }).code !== 'ENOENT') {
86
+ throw err
87
+ }
88
+ }
89
+
90
+ return registry
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Compile
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Load, validate, and write the pre-compiled JSON lookup file.
99
+ * Run this whenever `.claude/scaffold-registry.yml` changes.
100
+ *
101
+ * Called by the `pnpm scaffold:compile-registry` script (wired in Step 2 CI).
102
+ */
103
+ export function compileScaffoldRegistry(): ScaffoldRegistry {
104
+ const root = monorepoRoot()
105
+ const registry = loadScaffoldRegistryNoSyncCheck(root)
106
+
107
+ const jsonPath = path.join(root, JSON_FILENAME)
108
+ writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
109
+
110
+ return registry
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Lookup helpers (used by hooks for fast path matching)
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
119
+ * Falls back to YAML if the JSON is missing.
120
+ */
121
+ export function loadScaffoldRegistryFast(): ScaffoldRegistry {
122
+ const root = monorepoRoot()
123
+ const jsonPath = path.join(root, JSON_FILENAME)
124
+
125
+ try {
126
+ const raw = readFileSync(jsonPath, 'utf-8')
127
+ const parsed = JSON.parse(raw) as unknown
128
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
129
+ if (result.success) return result.data
130
+ } catch {
131
+ // fall through to YAML
132
+ }
133
+
134
+ return loadScaffoldRegistryNoSyncCheck(root)
135
+ }
136
+
137
+ /**
138
+ * Return all entries whose `sources` contain at least one pattern that matches
139
+ * the given file path. Pattern matching is a simple substring/glob-prefix check
140
+ * suitable for hook use; Step 3 will upgrade to full micromatch when the hook
141
+ * is implemented.
142
+ */
143
+ export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
144
+ return registry.entries.filter((entry) => entry.sources.some((pattern) => pathMatchesPattern(filePath, pattern)))
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Internal helpers
149
+ // ---------------------------------------------------------------------------
150
+
151
+ function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
152
+ const yamlPath = path.join(root, YAML_FILENAME)
153
+ let raw: string
154
+ try {
155
+ raw = readFileSync(yamlPath, 'utf-8')
156
+ } catch (err) {
157
+ throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
158
+ }
159
+ const parsed = parseYaml(raw) as unknown
160
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
161
+ if (!result.success) {
162
+ const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
163
+ throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
164
+ }
165
+ return result.data
166
+ }
167
+
168
+ /**
169
+ * Lightweight pattern match: handles exact paths, directory prefixes, and
170
+ * simple `*` wildcards at the end of a segment. Full micromatch lands in Step 3.
171
+ */
172
+ function pathMatchesPattern(filePath: string, pattern: string): boolean {
173
+ // Normalize separators
174
+ const normalizedFile = filePath.replace(/\\/g, '/')
175
+ const normalizedPattern = pattern.replace(/\\/g, '/')
176
+
177
+ // Exact match
178
+ if (normalizedFile === normalizedPattern) return true
179
+
180
+ // If pattern ends with `/**` or `/*`, check prefix
181
+ if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
182
+ const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
183
+ return normalizedFile.startsWith(prefix + '/')
184
+ }
185
+
186
+ // If pattern contains `*`, convert to simple regex
187
+ if (normalizedPattern.includes('*')) {
188
+ const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
189
+ return new RegExp(`^${escaped}$`).test(normalizedFile)
190
+ }
191
+
192
+ // Directory prefix match (pattern is a directory)
193
+ return normalizedFile.startsWith(normalizedPattern + '/')
194
+ }
@@ -0,0 +1,144 @@
1
+ import { z } from 'zod'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Kind taxonomy
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /**
8
+ * The kind of scaffold entry:
9
+ *
10
+ * - autogen: fully derivable from source; regen command is the fix
11
+ * - manual-scaffold: hand-authored structure that must be updated on source change
12
+ * - typed-id: typed-ID constants that encode semantic naming decisions
13
+ * - sync-preservation: external-template files with a declared sync tier
14
+ * - vibe-gated: paths protected by the pre-edit-vibe-gate hook
15
+ * - sdk-cli-generator: SDK CLI generators (generate-docs-index, generate-resources, etc.)
16
+ * - validator: drift-detection scripts or CI checks (meta.json pages[] validators, etc.)
17
+ * - other: escape hatch; requires free-form notes
18
+ */
19
+ export const ScaffoldEntryKindSchema = z.enum([
20
+ 'autogen',
21
+ 'manual-scaffold',
22
+ 'typed-id',
23
+ 'sync-preservation',
24
+ 'vibe-gated',
25
+ 'sdk-cli-generator',
26
+ 'validator',
27
+ 'other'
28
+ ])
29
+
30
+ export type ScaffoldEntryKind = z.infer<typeof ScaffoldEntryKindSchema>
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Scaffold reference (a single downstream artifact that needs attention)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export const ScaffoldRefSchema = z.object({
37
+ /**
38
+ * File path, glob, or symbolic target (e.g. "docs: sync-preservation-matrix").
39
+ * Symbolic targets begin with "docs:" or "autogen-target:".
40
+ */
41
+ path: z.string().min(1),
42
+
43
+ /**
44
+ * Command to regenerate, or "manual" if human judgment is required.
45
+ */
46
+ regen: z.string().min(1).optional(),
47
+
48
+ /**
49
+ * Optionally narrow the kind of this specific scaffold reference, when it
50
+ * differs from the parent entry's kind.
51
+ */
52
+ kind: ScaffoldEntryKindSchema.optional(),
53
+
54
+ /**
55
+ * Human-readable hint shown in reminder messages.
56
+ */
57
+ hint: z.string().optional()
58
+ })
59
+
60
+ export type ScaffoldRef = z.infer<typeof ScaffoldRefSchema>
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Registry entry
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export const ScaffoldRegistryEntrySchema = z.object({
67
+ /**
68
+ * Stable slug identifier for this entry (kebab-case).
69
+ * Referenced by hooks, CI checks, and the /scaffold verify skill.
70
+ */
71
+ id: z
72
+ .string()
73
+ .min(1)
74
+ .regex(/^[a-z0-9-]+$/, 'id must be kebab-case'),
75
+
76
+ /**
77
+ * The primary kind of this entry.
78
+ */
79
+ kind: ScaffoldEntryKindSchema,
80
+
81
+ /**
82
+ * Glob patterns (or exact paths) that, when edited, should trigger a
83
+ * reminder for the associated scaffolds. Uses micromatch / glob semantics.
84
+ */
85
+ sources: z.array(z.string().min(1)).min(1, 'At least one source pattern is required'),
86
+
87
+ /**
88
+ * Downstream artifacts that need attention when a source changes.
89
+ */
90
+ dependents: z.array(ScaffoldRefSchema).min(1, 'At least one dependent is required'),
91
+
92
+ /**
93
+ * Package or app path that owns this scaffold surface.
94
+ * Examples: "packages/core", "apps/api", "external/_template"
95
+ */
96
+ owner: z.string().min(1),
97
+
98
+ /**
99
+ * Free-form notes. Required when kind is "other".
100
+ */
101
+ notes: z.string().optional(),
102
+
103
+ /**
104
+ * The shell command used to regenerate this entry's outputs.
105
+ *
106
+ * Required for `kind: autogen` entries once Step 6 migration lands.
107
+ * Optional now so harness scaffolding (Step 5) can land before migrating
108
+ * existing generators. Validated as a non-empty string when present.
109
+ *
110
+ * Example: `"pnpm navigation:generate"`, `"pnpm scaffold:sync"`
111
+ */
112
+ regen: z.string().min(1).optional(),
113
+
114
+ /**
115
+ * Per-entry reminder throttle override in milliseconds.
116
+ * Defaults to the hook-level default (5 minutes = 300_000 ms) when omitted.
117
+ */
118
+ cooldown_ms: z.number().int().positive().optional(),
119
+
120
+ /**
121
+ * When true, the PostToolUse reminder hook will automatically run the `regen`
122
+ * command after emitting the reminder, rather than just advising the user.
123
+ * Defaults to false (advisory only). Only meaningful when `regen` is also set.
124
+ */
125
+ auto_regen: z.boolean().optional()
126
+ })
127
+
128
+ export type ScaffoldRegistryEntry = z.infer<typeof ScaffoldRegistryEntrySchema>
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Top-level registry document
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export const ScaffoldRegistrySchema = z.object({
135
+ /**
136
+ * Schema version for forward-compatibility detection.
137
+ * Bump when the shape changes in a breaking way.
138
+ */
139
+ version: z.string().default('1'),
140
+
141
+ entries: z.array(ScaffoldRegistryEntrySchema).min(1)
142
+ })
143
+
144
+ export type ScaffoldRegistry = z.infer<typeof ScaffoldRegistrySchema>
@@ -1267,10 +1267,8 @@ export type Database = {
1267
1267
  }
1268
1268
  deployments: {
1269
1269
  Row: {
1270
- compiled_docs: Json | null
1271
1270
  created_at: string
1272
1271
  deployment_version: string | null
1273
- documentation: Json | null
1274
1272
  error_message: string | null
1275
1273
  id: string
1276
1274
  organization_id: string
@@ -1282,10 +1280,8 @@ export type Database = {
1282
1280
  updated_at: string
1283
1281
  }
1284
1282
  Insert: {
1285
- compiled_docs?: Json | null
1286
1283
  created_at?: string
1287
1284
  deployment_version?: string | null
1288
- documentation?: Json | null
1289
1285
  error_message?: string | null
1290
1286
  id?: string
1291
1287
  organization_id: string
@@ -1297,10 +1293,8 @@ export type Database = {
1297
1293
  updated_at?: string
1298
1294
  }
1299
1295
  Update: {
1300
- compiled_docs?: Json | null
1301
1296
  created_at?: string
1302
1297
  deployment_version?: string | null
1303
- documentation?: Json | null
1304
1298
  error_message?: string | null
1305
1299
  id?: string
1306
1300
  organization_id?: string
@@ -1812,6 +1806,13 @@ export type Database = {
1812
1806
  updated_at?: string
1813
1807
  }
1814
1808
  Relationships: [
1809
+ {
1810
+ foreignKeyName: "fk_milestones_project"
1811
+ columns: ["project_id"]
1812
+ isOneToOne: false
1813
+ referencedRelation: "prj_projects"
1814
+ referencedColumns: ["id"]
1815
+ },
1815
1816
  {
1816
1817
  foreignKeyName: "prj_milestones_organization_id_fkey"
1817
1818
  columns: ["organization_id"]
@@ -1872,6 +1873,34 @@ export type Database = {
1872
1873
  type?: string
1873
1874
  }
1874
1875
  Relationships: [
1876
+ {
1877
+ foreignKeyName: "fk_notes_created_by"
1878
+ columns: ["created_by"]
1879
+ isOneToOne: false
1880
+ referencedRelation: "users"
1881
+ referencedColumns: ["id"]
1882
+ },
1883
+ {
1884
+ foreignKeyName: "fk_notes_milestone"
1885
+ columns: ["milestone_id"]
1886
+ isOneToOne: false
1887
+ referencedRelation: "prj_milestones"
1888
+ referencedColumns: ["id"]
1889
+ },
1890
+ {
1891
+ foreignKeyName: "fk_notes_project"
1892
+ columns: ["project_id"]
1893
+ isOneToOne: false
1894
+ referencedRelation: "prj_projects"
1895
+ referencedColumns: ["id"]
1896
+ },
1897
+ {
1898
+ foreignKeyName: "fk_notes_task"
1899
+ columns: ["task_id"]
1900
+ isOneToOne: false
1901
+ referencedRelation: "prj_tasks"
1902
+ referencedColumns: ["id"]
1903
+ },
1875
1904
  {
1876
1905
  foreignKeyName: "prj_notes_created_by_fkey"
1877
1906
  columns: ["created_by"]
@@ -1962,6 +1991,20 @@ export type Database = {
1962
1991
  updated_at?: string
1963
1992
  }
1964
1993
  Relationships: [
1994
+ {
1995
+ foreignKeyName: "fk_projects_company"
1996
+ columns: ["client_company_id"]
1997
+ isOneToOne: false
1998
+ referencedRelation: "acq_companies"
1999
+ referencedColumns: ["id"]
2000
+ },
2001
+ {
2002
+ foreignKeyName: "fk_projects_deal"
2003
+ columns: ["deal_id"]
2004
+ isOneToOne: false
2005
+ referencedRelation: "acq_deals"
2006
+ referencedColumns: ["id"]
2007
+ },
1965
2008
  {
1966
2009
  foreignKeyName: "prj_projects_client_company_id_fkey"
1967
2010
  columns: ["client_company_id"]
@@ -2044,6 +2087,27 @@ export type Database = {
2044
2087
  updated_at?: string
2045
2088
  }
2046
2089
  Relationships: [
2090
+ {
2091
+ foreignKeyName: "fk_tasks_milestone"
2092
+ columns: ["milestone_id"]
2093
+ isOneToOne: false
2094
+ referencedRelation: "prj_milestones"
2095
+ referencedColumns: ["id"]
2096
+ },
2097
+ {
2098
+ foreignKeyName: "fk_tasks_parent"
2099
+ columns: ["parent_task_id"]
2100
+ isOneToOne: false
2101
+ referencedRelation: "prj_tasks"
2102
+ referencedColumns: ["id"]
2103
+ },
2104
+ {
2105
+ foreignKeyName: "fk_tasks_project"
2106
+ columns: ["project_id"]
2107
+ isOneToOne: false
2108
+ referencedRelation: "prj_projects"
2109
+ referencedColumns: ["id"]
2110
+ },
2047
2111
  {
2048
2112
  foreignKeyName: "prj_tasks_milestone_id_fkey"
2049
2113
  columns: ["milestone_id"]