@compilr-dev/factory 0.1.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.
Files changed (64) hide show
  1. package/README.md +131 -0
  2. package/dist/factory/file-writer.d.ts +11 -0
  3. package/dist/factory/file-writer.js +21 -0
  4. package/dist/factory/list-toolkits-tool.d.ts +12 -0
  5. package/dist/factory/list-toolkits-tool.js +35 -0
  6. package/dist/factory/registry.d.ts +15 -0
  7. package/dist/factory/registry.js +28 -0
  8. package/dist/factory/scaffold-tool.d.ts +21 -0
  9. package/dist/factory/scaffold-tool.js +95 -0
  10. package/dist/factory/skill.d.ts +14 -0
  11. package/dist/factory/skill.js +146 -0
  12. package/dist/factory/tools.d.ts +14 -0
  13. package/dist/factory/tools.js +17 -0
  14. package/dist/index.d.ts +22 -0
  15. package/dist/index.js +25 -0
  16. package/dist/model/defaults.d.ts +10 -0
  17. package/dist/model/defaults.js +68 -0
  18. package/dist/model/naming.d.ts +12 -0
  19. package/dist/model/naming.js +80 -0
  20. package/dist/model/operations-entity.d.ts +35 -0
  21. package/dist/model/operations-entity.js +110 -0
  22. package/dist/model/operations-field.d.ts +33 -0
  23. package/dist/model/operations-field.js +104 -0
  24. package/dist/model/operations-relationship.d.ts +19 -0
  25. package/dist/model/operations-relationship.js +90 -0
  26. package/dist/model/operations-section.d.ts +32 -0
  27. package/dist/model/operations-section.js +35 -0
  28. package/dist/model/operations.d.ts +12 -0
  29. package/dist/model/operations.js +63 -0
  30. package/dist/model/persistence.d.ts +19 -0
  31. package/dist/model/persistence.js +40 -0
  32. package/dist/model/schema.d.ts +15 -0
  33. package/dist/model/schema.js +269 -0
  34. package/dist/model/tools.d.ts +14 -0
  35. package/dist/model/tools.js +380 -0
  36. package/dist/model/types.d.ts +70 -0
  37. package/dist/model/types.js +8 -0
  38. package/dist/toolkits/react-node/api.d.ts +8 -0
  39. package/dist/toolkits/react-node/api.js +198 -0
  40. package/dist/toolkits/react-node/config.d.ts +9 -0
  41. package/dist/toolkits/react-node/config.js +120 -0
  42. package/dist/toolkits/react-node/dashboard.d.ts +8 -0
  43. package/dist/toolkits/react-node/dashboard.js +60 -0
  44. package/dist/toolkits/react-node/entity.d.ts +8 -0
  45. package/dist/toolkits/react-node/entity.js +469 -0
  46. package/dist/toolkits/react-node/helpers.d.ts +27 -0
  47. package/dist/toolkits/react-node/helpers.js +71 -0
  48. package/dist/toolkits/react-node/index.d.ts +8 -0
  49. package/dist/toolkits/react-node/index.js +52 -0
  50. package/dist/toolkits/react-node/router.d.ts +8 -0
  51. package/dist/toolkits/react-node/router.js +49 -0
  52. package/dist/toolkits/react-node/seed.d.ts +11 -0
  53. package/dist/toolkits/react-node/seed.js +144 -0
  54. package/dist/toolkits/react-node/shared.d.ts +7 -0
  55. package/dist/toolkits/react-node/shared.js +119 -0
  56. package/dist/toolkits/react-node/shell.d.ts +8 -0
  57. package/dist/toolkits/react-node/shell.js +152 -0
  58. package/dist/toolkits/react-node/static.d.ts +8 -0
  59. package/dist/toolkits/react-node/static.js +105 -0
  60. package/dist/toolkits/react-node/types-gen.d.ts +8 -0
  61. package/dist/toolkits/react-node/types-gen.js +31 -0
  62. package/dist/toolkits/types.d.ts +22 -0
  63. package/dist/toolkits/types.js +6 -0
  64. package/package.json +51 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Operation Dispatcher
3
+ *
4
+ * Routes op → handler, manages revision + updatedAt.
5
+ */
6
+ import { addEntity, updateEntity, removeEntity, renameEntity, reorderEntities, } from './operations-entity.js';
7
+ import { addField, updateField, removeField, renameField, } from './operations-field.js';
8
+ import { addRelationship, removeRelationship, } from './operations-relationship.js';
9
+ import { updateIdentity, updateLayout, updateFeatures, updateTheme, updateTechStack, } from './operations-section.js';
10
+ function dispatchOperation(model, operation) {
11
+ switch (operation.op) {
12
+ // Entity operations
13
+ case 'addEntity':
14
+ return addEntity(model, operation);
15
+ case 'updateEntity':
16
+ return updateEntity(model, operation);
17
+ case 'removeEntity':
18
+ return removeEntity(model, operation);
19
+ case 'renameEntity':
20
+ return renameEntity(model, operation);
21
+ case 'reorderEntities':
22
+ return reorderEntities(model, operation);
23
+ // Field operations
24
+ case 'addField':
25
+ return addField(model, operation);
26
+ case 'updateField':
27
+ return updateField(model, operation);
28
+ case 'removeField':
29
+ return removeField(model, operation);
30
+ case 'renameField':
31
+ return renameField(model, operation);
32
+ // Relationship operations
33
+ case 'addRelationship':
34
+ return addRelationship(model, operation);
35
+ case 'removeRelationship':
36
+ return removeRelationship(model, operation);
37
+ // Section operations
38
+ case 'updateIdentity':
39
+ return updateIdentity(model, operation);
40
+ case 'updateLayout':
41
+ return updateLayout(model, operation);
42
+ case 'updateFeatures':
43
+ return updateFeatures(model, operation);
44
+ case 'updateTheme':
45
+ return updateTheme(model, operation);
46
+ case 'updateTechStack':
47
+ return updateTechStack(model, operation);
48
+ default:
49
+ throw new Error(`Unknown operation: ${operation.op}`);
50
+ }
51
+ }
52
+ export function applyOperation(model, operation) {
53
+ const result = dispatchOperation(model, operation);
54
+ // Increment revision and update timestamp
55
+ return {
56
+ ...result,
57
+ meta: {
58
+ ...result.meta,
59
+ revision: result.meta.revision + 1,
60
+ updatedAt: new Date().toISOString(),
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Model Store — Persistence Layer
3
+ *
4
+ * Reads and writes the ApplicationModel as a project document
5
+ * with doc_type: 'app-model' via IDocumentRepository.
6
+ */
7
+ import type { IDocumentRepository } from '@compilr-dev/sdk';
8
+ import type { ApplicationModel } from './types.js';
9
+ export declare class ModelStore {
10
+ private readonly documents;
11
+ private readonly projectId;
12
+ constructor(config: {
13
+ documents: IDocumentRepository;
14
+ projectId: number;
15
+ });
16
+ get(): Promise<ApplicationModel | null>;
17
+ save(model: ApplicationModel): Promise<void>;
18
+ exists(): Promise<boolean>;
19
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Model Store — Persistence Layer
3
+ *
4
+ * Reads and writes the ApplicationModel as a project document
5
+ * with doc_type: 'app-model' via IDocumentRepository.
6
+ */
7
+ const APP_MODEL_DOC_TYPE = 'app-model';
8
+ const APP_MODEL_TITLE = 'Application Model';
9
+ export class ModelStore {
10
+ documents;
11
+ projectId;
12
+ constructor(config) {
13
+ this.documents = config.documents;
14
+ this.projectId = config.projectId;
15
+ }
16
+ async get() {
17
+ const doc = await this.documents.getByType(this.projectId, APP_MODEL_DOC_TYPE);
18
+ if (!doc)
19
+ return null;
20
+ try {
21
+ return JSON.parse(doc.content);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async save(model) {
28
+ const content = JSON.stringify(model, null, 2);
29
+ await this.documents.upsert({
30
+ project_id: this.projectId,
31
+ doc_type: APP_MODEL_DOC_TYPE,
32
+ title: APP_MODEL_TITLE,
33
+ content,
34
+ });
35
+ }
36
+ async exists() {
37
+ const doc = await this.documents.getByType(this.projectId, APP_MODEL_DOC_TYPE);
38
+ return doc !== null;
39
+ }
40
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Application Model Validation
3
+ *
4
+ * Structural validation + business rule validation for referential integrity.
5
+ */
6
+ import type { ApplicationModel } from './types.js';
7
+ export interface ValidationError {
8
+ readonly path: string;
9
+ readonly message: string;
10
+ }
11
+ export interface ValidationResult {
12
+ readonly valid: boolean;
13
+ readonly errors: readonly ValidationError[];
14
+ }
15
+ export declare function validateModel(model: ApplicationModel): ValidationResult;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Application Model Validation
3
+ *
4
+ * Structural validation + business rule validation for referential integrity.
5
+ */
6
+ import { isCamelCase, isPascalCase, isValidHexColor } from './naming.js';
7
+ // =============================================================================
8
+ // Structural Validation
9
+ // =============================================================================
10
+ function validateIdentity(model) {
11
+ const errors = [];
12
+ const { identity } = model;
13
+ if (identity.name.trim().length === 0) {
14
+ errors.push({ path: 'identity.name', message: 'Name is required' });
15
+ }
16
+ if (identity.description.trim().length === 0) {
17
+ errors.push({ path: 'identity.description', message: 'Description is required' });
18
+ }
19
+ if (identity.version.trim().length === 0) {
20
+ errors.push({ path: 'identity.version', message: 'Version is required' });
21
+ }
22
+ return errors;
23
+ }
24
+ function validateField(field, entityName, fieldIndex) {
25
+ const errors = [];
26
+ const prefix = `entities[${entityName}].fields[${String(fieldIndex)}]`;
27
+ if (field.name.trim().length === 0) {
28
+ errors.push({ path: `${prefix}.name`, message: 'Field name is required' });
29
+ }
30
+ else if (!isCamelCase(field.name)) {
31
+ errors.push({
32
+ path: `${prefix}.name`,
33
+ message: `Field name "${field.name}" must be camelCase`,
34
+ });
35
+ }
36
+ if (field.label.trim().length === 0) {
37
+ errors.push({ path: `${prefix}.label`, message: 'Field label is required' });
38
+ }
39
+ const validTypes = ['string', 'number', 'boolean', 'date', 'enum'];
40
+ if (!validTypes.includes(field.type)) {
41
+ errors.push({ path: `${prefix}.type`, message: `Invalid field type "${field.type}"` });
42
+ }
43
+ if (field.type === 'enum') {
44
+ if (!field.enumValues || field.enumValues.length === 0) {
45
+ errors.push({
46
+ path: `${prefix}.enumValues`,
47
+ message: 'enumValues required for enum type',
48
+ });
49
+ }
50
+ }
51
+ else if (field.enumValues && field.enumValues.length > 0) {
52
+ errors.push({
53
+ path: `${prefix}.enumValues`,
54
+ message: 'enumValues only allowed for enum type',
55
+ });
56
+ }
57
+ if (field.defaultValue !== undefined) {
58
+ const valid = validateDefaultValue(field);
59
+ if (!valid) {
60
+ errors.push({
61
+ path: `${prefix}.defaultValue`,
62
+ message: `Default value type does not match field type "${field.type}"`,
63
+ });
64
+ }
65
+ }
66
+ return errors;
67
+ }
68
+ function validateDefaultValue(field) {
69
+ const { type, defaultValue } = field;
70
+ if (defaultValue === undefined)
71
+ return true;
72
+ switch (type) {
73
+ case 'string':
74
+ case 'date':
75
+ return typeof defaultValue === 'string';
76
+ case 'number':
77
+ return typeof defaultValue === 'number';
78
+ case 'boolean':
79
+ return typeof defaultValue === 'boolean';
80
+ case 'enum':
81
+ return (typeof defaultValue === 'string' && (field.enumValues?.includes(defaultValue) ?? false));
82
+ default:
83
+ return false;
84
+ }
85
+ }
86
+ function validateEntity(entity, index, allEntityNames) {
87
+ const errors = [];
88
+ const prefix = `entities[${entity.name.length > 0 ? entity.name : String(index)}]`;
89
+ if (entity.name.trim().length === 0) {
90
+ errors.push({ path: `${prefix}.name`, message: 'Entity name is required' });
91
+ }
92
+ else if (!isPascalCase(entity.name)) {
93
+ errors.push({
94
+ path: `${prefix}.name`,
95
+ message: `Entity name "${entity.name}" must be PascalCase`,
96
+ });
97
+ }
98
+ if (entity.pluralName.trim().length === 0) {
99
+ errors.push({ path: `${prefix}.pluralName`, message: 'Plural name is required' });
100
+ }
101
+ if (entity.icon.trim().length === 0) {
102
+ errors.push({ path: `${prefix}.icon`, message: 'Icon is required' });
103
+ }
104
+ if (entity.fields.length === 0) {
105
+ errors.push({ path: `${prefix}.fields`, message: 'Entity must have at least one field' });
106
+ }
107
+ else {
108
+ const fieldNames = new Set();
109
+ entity.fields.forEach((field, fi) => {
110
+ if (fieldNames.has(field.name)) {
111
+ errors.push({
112
+ path: `${prefix}.fields[${String(fi)}].name`,
113
+ message: `Duplicate field name "${field.name}"`,
114
+ });
115
+ }
116
+ fieldNames.add(field.name);
117
+ errors.push(...validateField(field, entity.name, fi));
118
+ });
119
+ }
120
+ if (entity.views.length === 0) {
121
+ errors.push({ path: `${prefix}.views`, message: 'Entity must have at least one view' });
122
+ }
123
+ else {
124
+ const validViews = ['card', 'list', 'detail'];
125
+ for (const view of entity.views) {
126
+ if (!validViews.includes(view)) {
127
+ errors.push({ path: `${prefix}.views`, message: `Invalid view "${view}"` });
128
+ }
129
+ }
130
+ }
131
+ const relKeys = new Set();
132
+ for (const rel of entity.relationships) {
133
+ const validTypes = ['belongsTo', 'hasMany'];
134
+ if (!validTypes.includes(rel.type)) {
135
+ errors.push({
136
+ path: `${prefix}.relationships`,
137
+ message: `Invalid relationship type "${rel.type}"`,
138
+ });
139
+ }
140
+ if (rel.target.trim().length === 0) {
141
+ errors.push({
142
+ path: `${prefix}.relationships`,
143
+ message: 'Relationship target is required',
144
+ });
145
+ }
146
+ else if (!allEntityNames.includes(rel.target)) {
147
+ errors.push({
148
+ path: `${prefix}.relationships`,
149
+ message: `Relationship target "${rel.target}" does not exist`,
150
+ });
151
+ }
152
+ if (rel.target === entity.name) {
153
+ errors.push({
154
+ path: `${prefix}.relationships`,
155
+ message: 'Self-referential relationships are not supported',
156
+ });
157
+ }
158
+ const key = `${rel.type}:${rel.target}`;
159
+ if (relKeys.has(key)) {
160
+ errors.push({
161
+ path: `${prefix}.relationships`,
162
+ message: `Duplicate relationship ${rel.type} → ${rel.target}`,
163
+ });
164
+ }
165
+ relKeys.add(key);
166
+ }
167
+ return errors;
168
+ }
169
+ function validateLayout(model) {
170
+ const errors = [];
171
+ const validShells = ['sidebar-header', 'header-only'];
172
+ if (!validShells.includes(model.layout.shell)) {
173
+ errors.push({
174
+ path: 'layout.shell',
175
+ message: `Invalid shell type "${model.layout.shell}"`,
176
+ });
177
+ }
178
+ return errors;
179
+ }
180
+ function validateFeatures(model) {
181
+ const errors = [];
182
+ const featureKeys = [
183
+ 'dashboard',
184
+ 'auth',
185
+ 'userProfiles',
186
+ 'settings',
187
+ 'darkMode',
188
+ ];
189
+ for (const key of featureKeys) {
190
+ if (typeof model.features[key] !== 'boolean') {
191
+ errors.push({
192
+ path: `features.${key}`,
193
+ message: `Feature "${key}" must be a boolean`,
194
+ });
195
+ }
196
+ }
197
+ return errors;
198
+ }
199
+ function validateTheme(model) {
200
+ const errors = [];
201
+ if (!isValidHexColor(model.theme.primaryColor)) {
202
+ errors.push({
203
+ path: 'theme.primaryColor',
204
+ message: `Invalid hex color "${model.theme.primaryColor}" (expected #RRGGBB)`,
205
+ });
206
+ }
207
+ return errors;
208
+ }
209
+ function validateTechStack(model) {
210
+ const errors = [];
211
+ if (model.techStack.toolkit.trim().length === 0) {
212
+ errors.push({
213
+ path: 'techStack.toolkit',
214
+ message: 'Toolkit is required',
215
+ });
216
+ }
217
+ return errors;
218
+ }
219
+ function validateMeta(model) {
220
+ const errors = [];
221
+ if (model.meta.revision < 1) {
222
+ errors.push({
223
+ path: 'meta.revision',
224
+ message: 'Revision must be a positive number',
225
+ });
226
+ }
227
+ if (model.meta.createdAt.length === 0) {
228
+ errors.push({ path: 'meta.createdAt', message: 'createdAt is required' });
229
+ }
230
+ if (model.meta.updatedAt.length === 0) {
231
+ errors.push({ path: 'meta.updatedAt', message: 'updatedAt is required' });
232
+ }
233
+ if (model.meta.factoryVersion.length === 0) {
234
+ errors.push({ path: 'meta.factoryVersion', message: 'factoryVersion is required' });
235
+ }
236
+ return errors;
237
+ }
238
+ // =============================================================================
239
+ // Public API
240
+ // =============================================================================
241
+ export function validateModel(model) {
242
+ const errors = [];
243
+ errors.push(...validateIdentity(model));
244
+ // Entity uniqueness
245
+ const entityNames = model.entities.map((e) => e.name);
246
+ const entityNameSet = new Set();
247
+ for (const name of entityNames) {
248
+ if (entityNameSet.has(name)) {
249
+ errors.push({
250
+ path: 'entities',
251
+ message: `Duplicate entity name "${name}"`,
252
+ });
253
+ }
254
+ entityNameSet.add(name);
255
+ }
256
+ // Per-entity validation
257
+ model.entities.forEach((entity, i) => {
258
+ errors.push(...validateEntity(entity, i, entityNames));
259
+ });
260
+ errors.push(...validateLayout(model));
261
+ errors.push(...validateFeatures(model));
262
+ errors.push(...validateTheme(model));
263
+ errors.push(...validateTechStack(model));
264
+ errors.push(...validateMeta(model));
265
+ return {
266
+ valid: errors.length === 0,
267
+ errors,
268
+ };
269
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Model Tools
3
+ *
4
+ * app_model_get — scoped reads of the Application Model
5
+ * app_model_update — semantic operations on the model
6
+ * app_model_validate — check model health
7
+ */
8
+ import type { Tool } from '@compilr-dev/agents';
9
+ import type { PlatformContext } from '@compilr-dev/sdk';
10
+ export interface ModelToolsConfig {
11
+ readonly context: PlatformContext;
12
+ readonly cwd?: string;
13
+ }
14
+ export declare function createModelTools(config: ModelToolsConfig): Tool<never>[];