anhedral 0.4.2 → 0.6.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 (89) hide show
  1. package/README.md +35 -265
  2. package/assets/anhedral-cli-init-technical.svg +322 -0
  3. package/assets/anhedral-cli-init.svg +257 -173
  4. package/dist/bin.js +28 -191
  5. package/dist/dependencies.d.ts +7 -57
  6. package/dist/dependencies.js +96 -308
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/platforms/desktop.js +401 -0
  10. package/dist/platforms/extension.js +401 -0
  11. package/dist/platforms/mobile.js +232 -0
  12. package/dist/project.d.ts +0 -26
  13. package/dist/standard-products.d.ts +2 -0
  14. package/dist/standard-products.js +6 -0
  15. package/dist/standard.d.ts +18 -0
  16. package/dist/standard.js +595 -0
  17. package/dist/version.d.ts +1 -1
  18. package/dist/version.js +1 -1
  19. package/docs/application-stack-standard.md +700 -0
  20. package/package.json +29 -51
  21. package/CHANGELOG.md +0 -178
  22. package/CONTRIBUTING.md +0 -77
  23. package/SECURITY.md +0 -37
  24. package/SKILL.md +0 -339
  25. package/assets/images/svg/ably-symbol-color.svg +0 -8
  26. package/bin/anhedral.js +0 -2
  27. package/dist/architecture/contributions.d.ts +0 -28
  28. package/dist/architecture/contributions.js +0 -125
  29. package/dist/architecture/index.d.ts +0 -12
  30. package/dist/architecture/index.js +0 -6
  31. package/dist/architecture/manifest.d.ts +0 -53
  32. package/dist/architecture/manifest.js +0 -283
  33. package/dist/architecture/modules.d.ts +0 -41
  34. package/dist/architecture/modules.js +0 -187
  35. package/dist/architecture/plan.d.ts +0 -58
  36. package/dist/architecture/plan.js +0 -145
  37. package/dist/architecture/templates.d.ts +0 -10
  38. package/dist/architecture/templates.js +0 -23
  39. package/dist/cli.d.ts +0 -32
  40. package/dist/cli.js +0 -281
  41. package/dist/composers/root.d.ts +0 -6
  42. package/dist/composers/root.js +0 -35
  43. package/dist/print.d.ts +0 -8
  44. package/dist/print.js +0 -48
  45. package/dist/prompts.d.ts +0 -6
  46. package/dist/prompts.js +0 -41
  47. package/dist/scaffold.d.ts +0 -64
  48. package/dist/scaffold.js +0 -2102
  49. package/dist/template-source.d.ts +0 -20
  50. package/dist/template-source.js +0 -180
  51. package/dist/templates/api.d.ts +0 -2
  52. package/dist/templates/api.js +0 -3187
  53. package/dist/templates/assets-private-proxy.d.ts +0 -3
  54. package/dist/templates/assets-private-proxy.js +0 -277
  55. package/dist/templates/desktop.js +0 -792
  56. package/dist/templates/electron-updater.d.ts +0 -3
  57. package/dist/templates/electron-updater.js +0 -358
  58. package/dist/templates/extension.js +0 -856
  59. package/dist/templates/first-run.d.ts +0 -2
  60. package/dist/templates/first-run.js +0 -167
  61. package/dist/templates/mobile.js +0 -861
  62. package/dist/templates/shared.d.ts +0 -2
  63. package/dist/templates/shared.js +0 -577
  64. package/dist/templates/web.d.ts +0 -2
  65. package/dist/templates/web.js +0 -569
  66. package/dist/toolchain.d.ts +0 -3
  67. package/dist/toolchain.js +0 -8
  68. package/dist/ui.d.ts +0 -33
  69. package/dist/ui.js +0 -199
  70. package/dist/workspace-config.d.ts +0 -11
  71. package/dist/workspace-config.js +0 -372
  72. package/docs/NORTHSTAR.md +0 -114
  73. package/docs/conventions.md +0 -94
  74. package/docs/engineering-principles.md +0 -88
  75. package/docs/master-stack-map.md +0 -199
  76. package/docs/output-tree-contract.md +0 -493
  77. package/docs/references/manual-scaffolding.md +0 -421
  78. package/docs/references/provisioning.md +0 -577
  79. package/scripts/render-master-stack-map.mjs +0 -510
  80. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  81. package/templates/catalog.json +0 -23
  82. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  83. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  84. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  85. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  86. package/templates/web-next/apps/web/next-env.d.ts +0 -6
  87. /package/dist/{templates → platforms}/desktop.d.ts +0 -0
  88. /package/dist/{templates → platforms}/extension.d.ts +0 -0
  89. /package/dist/{templates → platforms}/mobile.d.ts +0 -0
@@ -1,283 +0,0 @@
1
- import { DEFAULT_MODULE_REGISTRY, isModuleId, resolveModules, } from './modules.js';
2
- import { isFileOwnershipClass, isSha256, normalizePlanPath, } from './plan.js';
3
- import { isTemplateId, templateIdsForModules, } from './templates.js';
4
- import { isNativeStylingLibrary, isUiProvider, isUiTarget, normalizeUiComponentName, providerForTarget, uiInstallKey, } from '../ui.js';
5
- export const MANIFEST_SCHEMA_VERSION = 5;
6
- export class ManifestValidationError extends Error {
7
- code;
8
- path;
9
- constructor(code, message, path) {
10
- super(message);
11
- this.code = code;
12
- this.path = path;
13
- this.name = 'ManifestValidationError';
14
- }
15
- }
16
- function isRecord(value) {
17
- return typeof value === 'object' && value !== null && !Array.isArray(value);
18
- }
19
- function readNonEmptyString(value, path) {
20
- if (typeof value !== 'string' || value.trim().length === 0) {
21
- throw new ManifestValidationError('INVALID_MANIFEST', `${path} must be a non-empty string`, path);
22
- }
23
- return value;
24
- }
25
- function assertExactKeys(value, expected, path, code = 'INVALID_MANIFEST') {
26
- const expectedKeys = new Set(expected);
27
- const actual = Object.keys(value);
28
- const missing = expected.filter((key) => !Object.hasOwn(value, key));
29
- const unknown = actual.filter((key) => !expectedKeys.has(key));
30
- if (missing.length === 0 && unknown.length === 0)
31
- return;
32
- const details = [
33
- missing.length > 0 ? `missing ${missing.join(', ')}` : null,
34
- unknown.length > 0 ? `unknown ${unknown.join(', ')}` : null,
35
- ].filter((entry) => entry !== null).join('; ');
36
- throw new ManifestValidationError(code, `${path} has invalid fields (${details})`, path);
37
- }
38
- function parseModuleArray(value) {
39
- if (!Array.isArray(value)) {
40
- throw new ManifestValidationError('INVALID_MANIFEST', 'modules must be an array', 'modules');
41
- }
42
- const seen = new Set();
43
- const modules = [];
44
- for (let index = 0; index < value.length; index += 1) {
45
- const entry = value[index];
46
- if (!isModuleId(entry)) {
47
- throw new ManifestValidationError('UNKNOWN_MODULE', `modules[${index}] is not a supported module: ${String(entry)}`, `modules[${index}]`);
48
- }
49
- if (seen.has(entry)) {
50
- throw new ManifestValidationError('DUPLICATE_MODULE', `modules contains duplicate module ${entry}`, 'modules');
51
- }
52
- seen.add(entry);
53
- modules.push(entry);
54
- }
55
- return Object.freeze(modules);
56
- }
57
- function assertExactModuleClosure(modules, registry) {
58
- let expected;
59
- try {
60
- expected = resolveModules(modules, registry).resolvedModules;
61
- }
62
- catch (error) {
63
- throw new ManifestValidationError('INVALID_MANIFEST', error instanceof Error ? error.message : 'Manifest modules are invalid', 'modules');
64
- }
65
- if (expected.length !== modules.length || expected.some((moduleId, index) => modules[index] !== moduleId)) {
66
- throw new ManifestValidationError('INVALID_MODULE_CLOSURE', `modules must be the complete, canonically ordered dependency closure: ${expected.join(', ')}`, 'modules');
67
- }
68
- }
69
- function readFileRecord(value, path, modules) {
70
- if (!isRecord(value)) {
71
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path} must be an object`, path);
72
- }
73
- assertExactKeys(value, ['owner', 'ownership', 'hash', 'mode'], path, 'INVALID_FILE_RECORD');
74
- const owner = value.owner;
75
- if (owner !== 'root' && !isModuleId(owner)) {
76
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.owner is invalid`, `${path}.owner`);
77
- }
78
- if (owner !== 'root' && !modules.has(owner)) {
79
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.owner references an unselected module: ${owner}`, `${path}.owner`);
80
- }
81
- if (!isFileOwnershipClass(value.ownership)) {
82
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.ownership is invalid`, `${path}.ownership`);
83
- }
84
- if (!isSha256(value.hash)) {
85
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.hash must be a SHA-256 hex digest`, `${path}.hash`);
86
- }
87
- const mode = value.mode;
88
- if (mode !== null && (!Number.isInteger(mode) || mode < 0 || mode > 0o777)) {
89
- throw new ManifestValidationError('INVALID_FILE_RECORD', `${path}.mode must be null or normalized Unix permission bits between 0 and 511`, `${path}.mode`);
90
- }
91
- return Object.freeze({
92
- owner,
93
- ownership: value.ownership,
94
- hash: value.hash,
95
- mode: mode,
96
- });
97
- }
98
- function readFiles(value, modules) {
99
- if (!isRecord(value)) {
100
- throw new ManifestValidationError('INVALID_MANIFEST', 'files must be an object', 'files');
101
- }
102
- const moduleSet = new Set(modules);
103
- const entries = [];
104
- for (const rawPath of Object.keys(value).sort()) {
105
- let filePath;
106
- try {
107
- filePath = normalizePlanPath(rawPath);
108
- }
109
- catch {
110
- throw new ManifestValidationError('INVALID_FILE_RECORD', `Invalid manifest file path: ${rawPath}`, `files.${rawPath}`);
111
- }
112
- if (filePath !== rawPath) {
113
- throw new ManifestValidationError('INVALID_FILE_RECORD', `Manifest file path is not canonical: ${rawPath}`, `files.${rawPath}`);
114
- }
115
- entries.push([filePath, readFileRecord(value[rawPath], `files.${rawPath}`, moduleSet)]);
116
- }
117
- return Object.freeze(Object.fromEntries(entries));
118
- }
119
- function readTemplates(value, modules) {
120
- if (!isRecord(value)) {
121
- throw new ManifestValidationError('INVALID_MANIFEST', 'templates must be an object', 'templates');
122
- }
123
- const entries = [];
124
- const expectedIds = [...templateIdsForModules(modules)].sort();
125
- const actualIds = Object.keys(value).sort();
126
- if (expectedIds.length !== actualIds.length
127
- || expectedIds.some((id, index) => id !== actualIds[index])) {
128
- throw new ManifestValidationError('INVALID_MANIFEST', `templates must exactly match selected modules: ${expectedIds.join(', ')}`, 'templates');
129
- }
130
- for (const id of Object.keys(value).sort()) {
131
- if (!isTemplateId(id)) {
132
- throw new ManifestValidationError('INVALID_MANIFEST', `Unknown template: ${id}`, `templates.${id}`);
133
- }
134
- const entry = value[id];
135
- if (!isRecord(entry)) {
136
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id} must be an object`, `templates.${id}`);
137
- }
138
- assertExactKeys(entry, ['version', 'sha256'], `templates.${id}`);
139
- if (!Number.isInteger(entry.version) || entry.version < 1) {
140
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id}.version must be a positive integer`, `templates.${id}.version`);
141
- }
142
- if (!isSha256(entry.sha256)) {
143
- throw new ManifestValidationError('INVALID_MANIFEST', `templates.${id}.sha256 must be a SHA-256 hex digest`, `templates.${id}.sha256`);
144
- }
145
- entries.push([id, Object.freeze({ version: entry.version, sha256: entry.sha256 })]);
146
- }
147
- return Object.freeze(Object.fromEntries(entries));
148
- }
149
- function readUi(value, modules) {
150
- if (!isRecord(value)) {
151
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui must be an object', 'ui');
152
- }
153
- assertExactKeys(value, ['nativeStyling', 'components'], 'ui');
154
- if (!isNativeStylingLibrary(value.nativeStyling)) {
155
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.nativeStyling must be nativewind or uniwind', 'ui.nativeStyling');
156
- }
157
- if (!Array.isArray(value.components)) {
158
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.components must be an array', 'ui.components');
159
- }
160
- const selectedModules = new Set(modules);
161
- const seen = new Set();
162
- const components = [];
163
- for (let index = 0; index < value.components.length; index += 1) {
164
- const itemPath = `ui.components[${index}]`;
165
- const item = value.components[index];
166
- if (!isRecord(item)) {
167
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath} must be an object`, itemPath);
168
- }
169
- assertExactKeys(item, ['name', 'target', 'provider', 'source', 'variant'], itemPath);
170
- let name;
171
- try {
172
- name = normalizeUiComponentName(readNonEmptyString(item.name, `${itemPath}.name`));
173
- }
174
- catch (error) {
175
- throw new ManifestValidationError('INVALID_MANIFEST', error instanceof Error ? error.message : `${itemPath}.name is invalid`, `${itemPath}.name`);
176
- }
177
- if (!isUiTarget(item.target) || !selectedModules.has(item.target)) {
178
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.target is not a selected UI client`, `${itemPath}.target`);
179
- }
180
- if (!isUiProvider(item.provider) || item.provider !== providerForTarget(item.target)) {
181
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.provider does not match its target`, `${itemPath}.provider`);
182
- }
183
- const source = readNonEmptyString(item.source, `${itemPath}.source`);
184
- const variant = item.variant;
185
- if (item.target === 'mobile') {
186
- if (!isNativeStylingLibrary(variant) || variant !== value.nativeStyling) {
187
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.variant must match ui.nativeStyling`, `${itemPath}.variant`);
188
- }
189
- }
190
- else if (variant !== null) {
191
- throw new ManifestValidationError('INVALID_MANIFEST', `${itemPath}.variant must be null for DOM clients`, `${itemPath}.variant`);
192
- }
193
- const install = Object.freeze({ name, target: item.target, provider: item.provider, source, variant });
194
- const key = uiInstallKey(install);
195
- if (seen.has(key)) {
196
- throw new ManifestValidationError('INVALID_MANIFEST', `Duplicate UI component installation: ${key}`, itemPath);
197
- }
198
- seen.add(key);
199
- components.push(install);
200
- }
201
- const canonical = [...components].sort((left, right) => uiInstallKey(left).localeCompare(uiInstallKey(right)));
202
- if (canonical.some((component, index) => component !== components[index])) {
203
- throw new ManifestValidationError('INVALID_MANIFEST', 'ui.components must be canonically sorted', 'ui.components');
204
- }
205
- return Object.freeze({ nativeStyling: value.nativeStyling, components: Object.freeze(components) });
206
- }
207
- function parseInput(input) {
208
- if (typeof input !== 'string')
209
- return input;
210
- try {
211
- return JSON.parse(input);
212
- }
213
- catch (error) {
214
- throw new ManifestValidationError('INVALID_JSON', `Manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, '$');
215
- }
216
- }
217
- export function readManifest(input, registry = DEFAULT_MODULE_REGISTRY) {
218
- const value = parseInput(input);
219
- if (!isRecord(value)) {
220
- throw new ManifestValidationError('INVALID_MANIFEST', 'Manifest must be an object', '$');
221
- }
222
- const schemaVersion = value.schemaVersion;
223
- if (typeof schemaVersion !== 'number' || !Number.isInteger(schemaVersion) || schemaVersion < 1) {
224
- throw new ManifestValidationError('INVALID_SCHEMA_VERSION', `schemaVersion must be exactly ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
225
- }
226
- if (schemaVersion > MANIFEST_SCHEMA_VERSION) {
227
- throw new ManifestValidationError('FUTURE_SCHEMA_VERSION', `Manifest schema ${schemaVersion} is newer than supported schema ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
228
- }
229
- if (schemaVersion !== MANIFEST_SCHEMA_VERSION) {
230
- throw new ManifestValidationError('INVALID_SCHEMA_VERSION', `Unsupported manifest schema: ${schemaVersion}; expected ${MANIFEST_SCHEMA_VERSION}`, 'schemaVersion');
231
- }
232
- assertExactKeys(value, ['schemaVersion', 'generatorVersion', 'project', 'modules', 'toolchain', 'templates', 'ui', 'files'], '$');
233
- if (!isRecord(value.project)) {
234
- throw new ManifestValidationError('INVALID_MANIFEST', 'project must be an object', 'project');
235
- }
236
- assertExactKeys(value.project, ['name', 'displayName'], 'project');
237
- const project = Object.freeze({
238
- name: readNonEmptyString(value.project.name, 'project.name'),
239
- displayName: readNonEmptyString(value.project.displayName, 'project.displayName'),
240
- });
241
- const modules = parseModuleArray(value.modules);
242
- assertExactModuleClosure(modules, registry);
243
- if (value.toolchain !== 'stable' && value.toolchain !== 'latest') {
244
- throw new ManifestValidationError('INVALID_MANIFEST', 'toolchain must be either stable or latest', 'toolchain');
245
- }
246
- return Object.freeze({
247
- schemaVersion: MANIFEST_SCHEMA_VERSION,
248
- generatorVersion: readNonEmptyString(value.generatorVersion, 'generatorVersion'),
249
- project,
250
- modules,
251
- toolchain: value.toolchain,
252
- templates: readTemplates(value.templates, modules),
253
- ui: readUi(value.ui, modules),
254
- files: readFiles(value.files, modules),
255
- });
256
- }
257
- export function createManifest(input) {
258
- const files = Object.fromEntries(input.plan.files.map((file) => [
259
- file.path,
260
- {
261
- owner: file.owner,
262
- ownership: file.ownership,
263
- hash: file.contentHash,
264
- mode: null,
265
- },
266
- ]));
267
- return readManifest({
268
- schemaVersion: MANIFEST_SCHEMA_VERSION,
269
- generatorVersion: input.generatorVersion,
270
- project: input.project,
271
- modules: input.plan.resolvedModules,
272
- toolchain: input.toolchain,
273
- templates: input.templates,
274
- ui: {
275
- nativeStyling: input.nativeStyling ?? 'nativewind',
276
- components: input.components ?? [],
277
- },
278
- files,
279
- }, input.registry ?? DEFAULT_MODULE_REGISTRY);
280
- }
281
- export function serializeManifest(manifest) {
282
- return `${JSON.stringify(manifest, null, 2)}\n`;
283
- }
@@ -1,41 +0,0 @@
1
- export declare const APP_MODULES: readonly ["web", "mobile", "api", "desktop", "extension"];
2
- export declare const FEATURE_MODULES: readonly ["db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
3
- export declare const MODULE_IDS: readonly ["web", "mobile", "api", "desktop", "extension", "db", "auth", "billing", "storage", "native-subscriptions", "electron-updater"];
4
- export type AppModule = (typeof APP_MODULES)[number];
5
- export type FeatureModule = (typeof FEATURE_MODULES)[number];
6
- export type ModuleId = (typeof MODULE_IDS)[number];
7
- export type ModuleKind = 'app' | 'feature';
8
- export type ModuleDefinition = {
9
- readonly id: ModuleId;
10
- readonly kind: ModuleKind;
11
- readonly requires: readonly ModuleId[];
12
- readonly conflicts: readonly ModuleId[];
13
- };
14
- export type ModuleRegistry = Readonly<Record<ModuleId, ModuleDefinition>>;
15
- export type DependencyEdge = {
16
- readonly module: ModuleId;
17
- readonly requires: ModuleId;
18
- };
19
- export type ModuleResolution = {
20
- readonly requestedModules: readonly ModuleId[];
21
- readonly resolvedModules: readonly ModuleId[];
22
- readonly dependencyAddedModules: readonly ModuleId[];
23
- readonly dependencyEdges: readonly DependencyEdge[];
24
- };
25
- export type ModuleRegistryErrorCode = 'DUPLICATE_MODULE_DEFINITION' | 'MISSING_MODULE_DEFINITION' | 'UNKNOWN_MODULE_REFERENCE' | 'SELF_DEPENDENCY' | 'SELF_CONFLICT' | 'DEPENDENCY_CYCLE';
26
- export declare class ModuleRegistryError extends Error {
27
- readonly code: ModuleRegistryErrorCode;
28
- readonly modules: readonly string[];
29
- constructor(code: ModuleRegistryErrorCode, message: string, modules?: readonly string[]);
30
- }
31
- export type ModuleResolutionErrorCode = 'UNKNOWN_MODULE' | 'MODULE_CONFLICT';
32
- export declare class ModuleResolutionError extends Error {
33
- readonly code: ModuleResolutionErrorCode;
34
- readonly modules: readonly string[];
35
- constructor(code: ModuleResolutionErrorCode, message: string, modules: readonly string[]);
36
- }
37
- export declare function isModuleId(value: unknown): value is ModuleId;
38
- export declare function createModuleRegistry(definitions: readonly ModuleDefinition[]): ModuleRegistry;
39
- export declare const DEFAULT_MODULE_DEFINITIONS: readonly ModuleDefinition[];
40
- export declare const DEFAULT_MODULE_REGISTRY: Readonly<Record<"electron-updater" | "web" | "mobile" | "desktop" | "extension" | "auth" | "billing" | "storage" | "api" | "db" | "native-subscriptions", ModuleDefinition>>;
41
- export declare function resolveModules(requested: readonly string[], registry?: ModuleRegistry): ModuleResolution;
@@ -1,187 +0,0 @@
1
- export const APP_MODULES = [
2
- 'web',
3
- 'mobile',
4
- 'api',
5
- 'desktop',
6
- 'extension',
7
- ];
8
- export const FEATURE_MODULES = [
9
- 'db',
10
- 'auth',
11
- 'billing',
12
- 'storage',
13
- 'native-subscriptions',
14
- 'electron-updater',
15
- ];
16
- export const MODULE_IDS = [...APP_MODULES, ...FEATURE_MODULES];
17
- export class ModuleRegistryError extends Error {
18
- code;
19
- modules;
20
- constructor(code, message, modules = []) {
21
- super(message);
22
- this.code = code;
23
- this.modules = modules;
24
- this.name = 'ModuleRegistryError';
25
- }
26
- }
27
- export class ModuleResolutionError extends Error {
28
- code;
29
- modules;
30
- constructor(code, message, modules) {
31
- super(message);
32
- this.code = code;
33
- this.modules = modules;
34
- this.name = 'ModuleResolutionError';
35
- }
36
- }
37
- const MODULE_ID_SET = new Set(MODULE_IDS);
38
- const MODULE_ORDER = new Map(MODULE_IDS.map((id, index) => [id, index]));
39
- export function isModuleId(value) {
40
- return typeof value === 'string' && MODULE_ID_SET.has(value);
41
- }
42
- function compareModules(left, right) {
43
- return (MODULE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER)
44
- - (MODULE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER);
45
- }
46
- function normalizeModuleList(values) {
47
- return Object.freeze([...new Set(values)].sort(compareModules));
48
- }
49
- function freezeDefinition(definition) {
50
- return Object.freeze({
51
- id: definition.id,
52
- kind: definition.kind,
53
- requires: normalizeModuleList(definition.requires),
54
- conflicts: normalizeModuleList(definition.conflicts),
55
- });
56
- }
57
- function assertNoDependencyCycles(registry) {
58
- const visited = new Set();
59
- const active = new Set();
60
- const stack = [];
61
- const visit = (moduleId) => {
62
- if (visited.has(moduleId))
63
- return;
64
- if (active.has(moduleId)) {
65
- const cycleStart = stack.indexOf(moduleId);
66
- const cycle = [...stack.slice(cycleStart), moduleId];
67
- throw new ModuleRegistryError('DEPENDENCY_CYCLE', `Module dependency cycle detected: ${cycle.join(' -> ')}`, Object.freeze(cycle));
68
- }
69
- active.add(moduleId);
70
- stack.push(moduleId);
71
- for (const required of registry[moduleId].requires)
72
- visit(required);
73
- stack.pop();
74
- active.delete(moduleId);
75
- visited.add(moduleId);
76
- };
77
- for (const moduleId of MODULE_IDS)
78
- visit(moduleId);
79
- }
80
- export function createModuleRegistry(definitions) {
81
- const registry = {};
82
- for (const definition of definitions) {
83
- if (!isModuleId(definition.id)) {
84
- throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Unknown module definition: ${String(definition.id)}`, Object.freeze([String(definition.id)]));
85
- }
86
- if (registry[definition.id]) {
87
- throw new ModuleRegistryError('DUPLICATE_MODULE_DEFINITION', `Duplicate module definition: ${definition.id}`, Object.freeze([definition.id]));
88
- }
89
- for (const required of definition.requires) {
90
- if (!isModuleId(required)) {
91
- throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Module ${definition.id} requires unknown module ${String(required)}`, Object.freeze([definition.id, String(required)]));
92
- }
93
- if (required === definition.id) {
94
- throw new ModuleRegistryError('SELF_DEPENDENCY', `Module ${definition.id} cannot require itself`, Object.freeze([definition.id]));
95
- }
96
- }
97
- for (const conflict of definition.conflicts) {
98
- if (!isModuleId(conflict)) {
99
- throw new ModuleRegistryError('UNKNOWN_MODULE_REFERENCE', `Module ${definition.id} conflicts with unknown module ${String(conflict)}`, Object.freeze([definition.id, String(conflict)]));
100
- }
101
- if (conflict === definition.id) {
102
- throw new ModuleRegistryError('SELF_CONFLICT', `Module ${definition.id} cannot conflict with itself`, Object.freeze([definition.id]));
103
- }
104
- }
105
- registry[definition.id] = freezeDefinition(definition);
106
- }
107
- const missing = MODULE_IDS.filter((moduleId) => !registry[moduleId]);
108
- if (missing.length > 0) {
109
- throw new ModuleRegistryError('MISSING_MODULE_DEFINITION', `Missing module definitions: ${missing.join(', ')}`, Object.freeze(missing));
110
- }
111
- const completeRegistry = Object.freeze(registry);
112
- assertNoDependencyCycles(completeRegistry);
113
- return completeRegistry;
114
- }
115
- const DEFAULT_MODULE_DEFINITION_INPUTS = [
116
- { id: 'web', kind: 'app', requires: [], conflicts: [] },
117
- { id: 'mobile', kind: 'app', requires: [], conflicts: [] },
118
- { id: 'api', kind: 'app', requires: [], conflicts: [] },
119
- { id: 'desktop', kind: 'app', requires: [], conflicts: [] },
120
- { id: 'extension', kind: 'app', requires: [], conflicts: [] },
121
- { id: 'db', kind: 'feature', requires: [], conflicts: [] },
122
- { id: 'auth', kind: 'feature', requires: ['api', 'db'], conflicts: [] },
123
- { id: 'billing', kind: 'feature', requires: ['auth'], conflicts: [] },
124
- { id: 'storage', kind: 'feature', requires: ['auth'], conflicts: [] },
125
- { id: 'native-subscriptions', kind: 'feature', requires: ['mobile', 'billing'], conflicts: [] },
126
- { id: 'electron-updater', kind: 'feature', requires: ['desktop'], conflicts: [] },
127
- ];
128
- export const DEFAULT_MODULE_DEFINITIONS = Object.freeze(DEFAULT_MODULE_DEFINITION_INPUTS.map(freezeDefinition));
129
- export const DEFAULT_MODULE_REGISTRY = createModuleRegistry(DEFAULT_MODULE_DEFINITIONS);
130
- export function resolveModules(requested, registry = DEFAULT_MODULE_REGISTRY) {
131
- const unknown = [...new Set(requested.filter((moduleId) => !isModuleId(moduleId)))].sort();
132
- if (unknown.length > 0) {
133
- throw new ModuleResolutionError('UNKNOWN_MODULE', `Unknown modules: ${unknown.join(', ')}`, Object.freeze(unknown));
134
- }
135
- const requestedModules = normalizeModuleList(requested);
136
- const resolved = new Set();
137
- const visiting = new Set();
138
- const ordered = [];
139
- const visit = (moduleId) => {
140
- if (resolved.has(moduleId))
141
- return;
142
- if (visiting.has(moduleId)) {
143
- throw new ModuleRegistryError('DEPENDENCY_CYCLE', `Module dependency cycle reached while resolving ${moduleId}`, Object.freeze([moduleId]));
144
- }
145
- visiting.add(moduleId);
146
- for (const required of registry[moduleId].requires)
147
- visit(required);
148
- visiting.delete(moduleId);
149
- resolved.add(moduleId);
150
- ordered.push(moduleId);
151
- };
152
- for (const moduleId of requestedModules)
153
- visit(moduleId);
154
- const conflictPairs = new Set();
155
- for (const moduleId of ordered) {
156
- for (const conflict of registry[moduleId].conflicts) {
157
- if (!resolved.has(conflict))
158
- continue;
159
- const pair = [moduleId, conflict].sort(compareModules);
160
- conflictPairs.add(`${pair[0]}\0${pair[1]}`);
161
- }
162
- }
163
- if (conflictPairs.size > 0) {
164
- const conflicts = [...conflictPairs]
165
- .map((pair) => pair.split('\0'))
166
- .sort(([leftA, leftB], [rightA, rightB]) => compareModules(leftA, rightA) || compareModules(leftB, rightB));
167
- const modules = Object.freeze([...new Set(conflicts.flat())].sort(compareModules));
168
- throw new ModuleResolutionError('MODULE_CONFLICT', `Conflicting modules: ${conflicts.map(([left, right]) => `${left} + ${right}`).join(', ')}`, modules);
169
- }
170
- // Resolution must have one stable representation regardless of whether a
171
- // dependency was requested directly or discovered while walking another
172
- // module. Without this final normalization, adding a module to an existing
173
- // project could produce a different manifest order than creating the same
174
- // topology directly (for example, adding electron-updater after db/auth).
175
- const resolvedModules = normalizeModuleList(ordered);
176
- const requestedSet = new Set(requestedModules);
177
- const dependencyAddedModules = resolvedModules.filter((moduleId) => !requestedSet.has(moduleId));
178
- const dependencyEdges = resolvedModules.flatMap((moduleId) => registry[moduleId].requires
179
- .filter((required) => resolved.has(required))
180
- .map((required) => Object.freeze({ module: moduleId, requires: required })));
181
- return Object.freeze({
182
- requestedModules,
183
- resolvedModules,
184
- dependencyAddedModules: Object.freeze(dependencyAddedModules),
185
- dependencyEdges: Object.freeze(dependencyEdges),
186
- });
187
- }
@@ -1,58 +0,0 @@
1
- import { type DependencyEdge, type ModuleId, type ModuleRegistry } from './modules.js';
2
- export declare const FILE_OWNERSHIP_CLASSES: readonly ["managed", "mergeable", "user"];
3
- export type FileOwnershipClass = (typeof FILE_OWNERSHIP_CLASSES)[number];
4
- export type FileOwner = ModuleId | 'root';
5
- export type GenerationOperation = 'init' | 'add';
6
- export type FileWritePolicy = 'replace-if-unmodified' | 'structural-merge' | 'create-only';
7
- export type PlanFileInput = {
8
- readonly path: string;
9
- readonly ownership: FileOwnershipClass;
10
- readonly content: string;
11
- };
12
- export type ModulePlanContribution = {
13
- readonly module: FileOwner;
14
- readonly files: readonly PlanFileInput[];
15
- };
16
- export type PlannedFile = {
17
- readonly path: string;
18
- readonly owner: FileOwner;
19
- readonly ownership: FileOwnershipClass;
20
- readonly writePolicy: FileWritePolicy;
21
- readonly content: string;
22
- readonly contentHash: string;
23
- };
24
- export type GenerationPlan = {
25
- readonly planVersion: 1;
26
- readonly operation: GenerationOperation;
27
- readonly requestedModules: readonly ModuleId[];
28
- readonly resolvedModules: readonly ModuleId[];
29
- readonly dependencyAddedModules: readonly ModuleId[];
30
- readonly dependencyEdges: readonly DependencyEdge[];
31
- readonly files: readonly PlannedFile[];
32
- readonly fingerprint: string;
33
- };
34
- export type BuildGenerationPlanInput = {
35
- readonly operation: GenerationOperation;
36
- readonly requestedModules: readonly string[];
37
- readonly contributions?: readonly ModulePlanContribution[];
38
- readonly registry?: ModuleRegistry;
39
- };
40
- export type PlanBuildErrorCode = 'INVALID_OPERATION' | 'INVALID_PLAN_PATH' | 'INVALID_FILE_OWNER' | 'INVALID_FILE_OWNERSHIP' | 'INVALID_FILE_CONTENT' | 'UNRESOLVED_CONTRIBUTION' | 'DUPLICATE_OUTPUT_PATH';
41
- export declare class PlanBuildError extends Error {
42
- readonly code: PlanBuildErrorCode;
43
- readonly paths: readonly string[];
44
- constructor(code: PlanBuildErrorCode, message: string, paths?: readonly string[]);
45
- }
46
- export declare function isFileOwnershipClass(value: unknown): value is FileOwnershipClass;
47
- export declare function isSha256(value: unknown): value is string;
48
- /**
49
- * Hash rendered text or exact filesystem bytes.
50
- *
51
- * Strings are encoded as UTF-8 for deterministic generation-plan fingerprints.
52
- * Callers hashing an existing file must pass its Buffer/Uint8Array so malformed
53
- * byte sequences cannot collapse to the same Unicode replacement characters.
54
- */
55
- export declare function hashContent(content: string | Uint8Array): string;
56
- export declare function writePolicyForOwnership(ownership: FileOwnershipClass): FileWritePolicy;
57
- export declare function normalizePlanPath(value: string): string;
58
- export declare function buildGenerationPlan(input: BuildGenerationPlanInput): GenerationPlan;