@devory/core 0.1.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devory/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared types, parsing utilities, and path configuration for AI Dev Factory",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,18 @@
1
+ # Devory default baseline: generic
2
+ # Stack-agnostic engineering standards distilled from core Devory doctrine.
3
+ # Referenced via: extends: "@devory/defaults/generic"
4
+
5
+ version: "1"
6
+
7
+ doctrine:
8
+ testing:
9
+ require_unit: true
10
+ require_integration: false
11
+ coverage_threshold: 80
12
+
13
+ architecture:
14
+ max_file_lines: 300
15
+ no_circular_deps: true
16
+
17
+ code_style:
18
+ prefer_explicit_over_clever: true
@@ -0,0 +1,29 @@
1
+ # Devory default baseline: TypeScript + Next.js
2
+ # Extends the generic baseline with Next.js app-router conventions.
3
+ # Referenced via: extends: "@devory/defaults/typescript-nextjs"
4
+
5
+ version: "1"
6
+
7
+ extends: "@devory/defaults/generic"
8
+
9
+ stack:
10
+ language: TypeScript
11
+ framework: Next.js
12
+
13
+ doctrine:
14
+ testing:
15
+ require_unit: true
16
+ require_integration: true
17
+ coverage_threshold: 80
18
+ avoid_mocking:
19
+ - database
20
+ - filesystem
21
+
22
+ architecture:
23
+ pattern: app-router
24
+ max_file_lines: 300
25
+ no_circular_deps: true
26
+
27
+ code_style:
28
+ no_any: true
29
+ prefer_explicit_over_clever: true
@@ -0,0 +1,28 @@
1
+ # Devory default baseline: TypeScript + Node.js
2
+ # Extends the generic baseline with Node.js API / service conventions.
3
+ # Referenced via: extends: "@devory/defaults/typescript-node"
4
+
5
+ version: "1"
6
+
7
+ extends: "@devory/defaults/generic"
8
+
9
+ stack:
10
+ language: TypeScript
11
+ framework: Node.js
12
+
13
+ doctrine:
14
+ testing:
15
+ require_unit: true
16
+ require_integration: true
17
+ coverage_threshold: 85
18
+ avoid_mocking:
19
+ - database
20
+
21
+ architecture:
22
+ pattern: layered
23
+ max_file_lines: 300
24
+ no_circular_deps: true
25
+
26
+ code_style:
27
+ no_any: true
28
+ prefer_explicit_over_clever: true
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @devory/core — public API
3
3
  *
4
- * Shared types, parsing utilities, and path configuration
5
- * for the AI Dev Factory monorepo.
4
+ * Shared types, parsing utilities, path configuration,
5
+ * engineering standards, and license tier detection.
6
6
  */
7
7
 
8
8
  export { parseFrontmatter } from "./parse.ts";
@@ -20,3 +20,28 @@ export type {
20
20
  FactoryPaths,
21
21
  FactoryRootSource,
22
22
  } from "./factory-environment.ts";
23
+ export {
24
+ loadStandards,
25
+ loadBaseline,
26
+ mergeStandards,
27
+ resolveBaselinePath,
28
+ serializeStandardsAsDoctrine,
29
+ STANDARDS_FILENAME,
30
+ } from "./standards.ts";
31
+ export type {
32
+ Standards,
33
+ StandardsStack,
34
+ StandardsTesting,
35
+ StandardsArchitecture,
36
+ StandardsCodeStyle,
37
+ StandardsDoctrine,
38
+ StandardsSource,
39
+ StandardsSourceType,
40
+ LoadedStandards,
41
+ } from "./standards.ts";
42
+ export {
43
+ detectTier,
44
+ isFeatureEnabled,
45
+ tierGateMessage,
46
+ } from "./license.ts";
47
+ export type { Tier, ProFeature, LicenseInfo } from "./license.ts";
package/src/license.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * packages/core/src/license.ts
3
+ *
4
+ * Tier detection and Pro feature gating for Devory.
5
+ *
6
+ * Tiers:
7
+ * Core — no license key required; default baselines only; custom_rules ignored
8
+ * Pro — license key enables custom_rules and baseline overrides
9
+ *
10
+ * Key resolution order:
11
+ * 1. DEVORY_LICENSE_KEY environment variable
12
+ * 2. .devory/license file in the factory root
13
+ * 3. No key found → Core
14
+ *
15
+ * Network verification is stubbed — a real call will be wired once the
16
+ * license service exists. Local validation (format check) runs synchronously
17
+ * so Core tier never blocks on any network call.
18
+ */
19
+
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export type Tier = "core" | "pro";
28
+
29
+ /** Features gated behind Pro tier. */
30
+ export type ProFeature = "custom_rules" | "baseline_overrides" | "shared_doctrine" | "pr_gates";
31
+
32
+ export interface LicenseInfo {
33
+ tier: Tier;
34
+ /** Raw key value, if one was found */
35
+ key?: string;
36
+ /** Where the key was found */
37
+ source?: "env" | "file";
38
+ /** True when a key was found but failed local format validation */
39
+ invalid?: boolean;
40
+ /** Human-readable explanation of the tier decision */
41
+ reason: string;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Constants
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const ENV_VAR = "DEVORY_LICENSE_KEY";
49
+ const LICENSE_FILE = path.join(".devory", "license");
50
+
51
+ /**
52
+ * Minimum length for a key to pass local format validation.
53
+ * Keys will follow a `devory_<tier>_<random>` convention once the license
54
+ * service is built; this floor rejects obvious junk values in the meantime.
55
+ */
56
+ const MIN_KEY_LENGTH = 16;
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Tier detection
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Detect the current license tier.
64
+ *
65
+ * @param factoryRoot Absolute path to the factory workspace root.
66
+ * When omitted, file-based key detection is skipped.
67
+ */
68
+ export function detectTier(factoryRoot?: string): LicenseInfo {
69
+ // 1. Environment variable
70
+ const envKey = process.env[ENV_VAR];
71
+ if (envKey !== undefined) {
72
+ return validateKey(envKey.trim(), "env");
73
+ }
74
+
75
+ // 2. .devory/license file
76
+ if (factoryRoot) {
77
+ const filePath = path.join(factoryRoot, LICENSE_FILE);
78
+ if (fs.existsSync(filePath)) {
79
+ const fileKey = fs.readFileSync(filePath, "utf-8").trim();
80
+ return validateKey(fileKey, "file");
81
+ }
82
+ }
83
+
84
+ // 3. No key — Core
85
+ return {
86
+ tier: "core",
87
+ reason: "No license key found — running on Core tier",
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Validate a raw key string and return the corresponding LicenseInfo.
93
+ * Currently performs local format validation only.
94
+ * Network verification is a no-op stub until the license service exists.
95
+ */
96
+ function validateKey(key: string, source: "env" | "file"): LicenseInfo {
97
+ if (!key || key.length < MIN_KEY_LENGTH) {
98
+ return {
99
+ tier: "core",
100
+ key,
101
+ source,
102
+ invalid: true,
103
+ reason: `License key from ${source === "env" ? "DEVORY_LICENSE_KEY" : ".devory/license"} is invalid (must be ≥ ${MIN_KEY_LENGTH} characters) — falling back to Core tier`,
104
+ };
105
+ }
106
+
107
+ // TODO: when the license service ships, verify the key here (once, cached
108
+ // locally in .devory/license-cache.json with an expiry timestamp).
109
+ return {
110
+ tier: "pro",
111
+ key,
112
+ source,
113
+ reason: `License key found via ${source === "env" ? "DEVORY_LICENSE_KEY" : ".devory/license"} — Pro tier active`,
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Feature gating
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Returns true if the given Pro feature is enabled for the current tier.
123
+ * Call this at each Pro feature boundary instead of comparing tier directly.
124
+ */
125
+ export function isFeatureEnabled(feature: ProFeature, info: LicenseInfo): boolean {
126
+ // All Pro features require Pro tier. Teams features would extend this check.
127
+ switch (feature) {
128
+ case "custom_rules":
129
+ case "baseline_overrides":
130
+ case "shared_doctrine":
131
+ case "pr_gates":
132
+ return info.tier === "pro";
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Produce a one-line advisory message shown to Core users when they have a
138
+ * Pro-only field configured. Shown once per command invocation, not per file.
139
+ */
140
+ export function tierGateMessage(feature: ProFeature): string {
141
+ const featureLabel: Record<ProFeature, string> = {
142
+ custom_rules: "custom_rules in devory.standards.yml",
143
+ baseline_overrides: "baseline overrides",
144
+ shared_doctrine: "shared doctrine",
145
+ pr_gates: "PR gates",
146
+ };
147
+ return (
148
+ `[devory] ${featureLabel[feature]} requires a Pro license — ` +
149
+ `set DEVORY_LICENSE_KEY or create .devory/license to upgrade. ` +
150
+ `This setting will be ignored on Core tier.`
151
+ );
152
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * packages/core/src/standards.ts
3
+ *
4
+ * Loads and types the devory.standards.yml configuration file.
5
+ *
6
+ * devory.standards.yml is the user-facing doctrine source — the structured
7
+ * definition of what "good" means for a given codebase. When present it takes
8
+ * precedence over the freeform brain/ markdown files.
9
+ *
10
+ * Entry points:
11
+ * loadStandards(factoryRoot) — load and parse the YAML file
12
+ * serializeStandardsAsDoctrine(s) — render as a string for AI context injection
13
+ */
14
+
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { load as parseYaml } from "js-yaml";
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Schema types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface StandardsStack {
28
+ language?: string;
29
+ framework?: string;
30
+ database?: string;
31
+ }
32
+
33
+ export interface StandardsTesting {
34
+ require_unit?: boolean;
35
+ require_integration?: boolean;
36
+ coverage_threshold?: number;
37
+ avoid_mocking?: string[];
38
+ }
39
+
40
+ export interface StandardsArchitecture {
41
+ pattern?: string;
42
+ max_file_lines?: number;
43
+ no_circular_deps?: boolean;
44
+ }
45
+
46
+ export interface StandardsCodeStyle {
47
+ no_any?: boolean;
48
+ prefer_explicit_over_clever?: boolean;
49
+ }
50
+
51
+ export interface StandardsDoctrine {
52
+ extends?: string;
53
+ testing?: StandardsTesting;
54
+ architecture?: StandardsArchitecture;
55
+ code_style?: StandardsCodeStyle;
56
+ /** Pro/Teams tier only — ignored on Core */
57
+ custom_rules?: string[];
58
+ }
59
+
60
+ export interface Standards {
61
+ version?: string;
62
+ stack?: StandardsStack;
63
+ doctrine?: StandardsDoctrine;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Source descriptor
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export type StandardsSourceType = "yaml" | "brain" | "none";
71
+
72
+ export interface StandardsSource {
73
+ type: StandardsSourceType;
74
+ /** Absolute path to devory.standards.yml, if found */
75
+ path?: string;
76
+ }
77
+
78
+ export interface LoadedStandards {
79
+ standards: Standards;
80
+ source: StandardsSource;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Loader
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export const STANDARDS_FILENAME = "devory.standards.yml";
88
+
89
+ /**
90
+ * Load devory.standards.yml from the factory root.
91
+ * Returns null standards and source type "none" when the file does not exist.
92
+ * Throws a descriptive error if the file exists but cannot be parsed.
93
+ */
94
+ export function loadStandards(factoryRoot: string): LoadedStandards {
95
+ const filePath = path.join(factoryRoot, STANDARDS_FILENAME);
96
+
97
+ if (!fs.existsSync(filePath)) {
98
+ return { standards: {}, source: { type: "none" } };
99
+ }
100
+
101
+ const raw = fs.readFileSync(filePath, "utf-8");
102
+
103
+ let parsed: unknown;
104
+ try {
105
+ parsed = parseYaml(raw);
106
+ } catch (err) {
107
+ throw new Error(
108
+ `devory: failed to parse ${STANDARDS_FILENAME}: ${err instanceof Error ? err.message : String(err)}`
109
+ );
110
+ }
111
+
112
+ if (parsed === null || typeof parsed !== "object") {
113
+ throw new Error(`devory: ${STANDARDS_FILENAME} must be a YAML object, got: ${typeof parsed}`);
114
+ }
115
+
116
+ const user = parsed as Standards;
117
+ const extendsValue = (user as Standards & { extends?: string }).extends
118
+ ?? user.doctrine?.extends;
119
+
120
+ // Resolve extends chain if a bundled baseline is referenced
121
+ const standards = extendsValue
122
+ ? mergeStandards(loadBaseline(extendsValue), user)
123
+ : user;
124
+
125
+ return {
126
+ standards,
127
+ source: { type: "yaml", path: filePath },
128
+ };
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Baseline resolver
133
+ //
134
+ // Maps "@devory/defaults/<name>" to the bundled YAML file shipped with
135
+ // @devory/core. Users reference baselines via the `extends` field in
136
+ // devory.standards.yml or within baseline files themselves.
137
+ // ---------------------------------------------------------------------------
138
+
139
+ const DEVORY_DEFAULTS_PREFIX = "@devory/defaults/";
140
+ const DEFAULTS_DIR = path.join(__dirname, "defaults");
141
+
142
+ const KNOWN_BASELINES: Record<string, string> = {
143
+ generic: "generic.yml",
144
+ "typescript-nextjs": "typescript-nextjs.yml",
145
+ "typescript-node": "typescript-node.yml",
146
+ };
147
+
148
+ /**
149
+ * Resolve an extends string to an absolute path.
150
+ * Returns null if the baseline is not a bundled Devory baseline or does not exist.
151
+ */
152
+ export function resolveBaselinePath(extendsValue: string): string | null {
153
+ if (!extendsValue.startsWith(DEVORY_DEFAULTS_PREFIX)) return null;
154
+ const key = extendsValue.slice(DEVORY_DEFAULTS_PREFIX.length);
155
+ const filename = KNOWN_BASELINES[key];
156
+ if (!filename) return null;
157
+ return path.join(DEFAULTS_DIR, filename);
158
+ }
159
+
160
+ /**
161
+ * Load a bundled Devory baseline by its extends identifier.
162
+ * Recursively resolves the baseline's own extends chain (e.g. typescript-nextjs → generic).
163
+ * Returns an empty Standards object if the baseline cannot be found.
164
+ */
165
+ export function loadBaseline(extendsValue: string): Standards {
166
+ const filePath = resolveBaselinePath(extendsValue);
167
+ if (!filePath || !fs.existsSync(filePath)) return {};
168
+
169
+ const raw = fs.readFileSync(filePath, "utf-8");
170
+ const parsed = parseYaml(raw);
171
+ if (!parsed || typeof parsed !== "object") return {};
172
+
173
+ const baseline = parsed as Standards & { extends?: string };
174
+ const parentExtends = baseline.extends;
175
+
176
+ if (parentExtends) {
177
+ const parent = loadBaseline(parentExtends);
178
+ return mergeStandards(parent, baseline);
179
+ }
180
+
181
+ return baseline as Standards;
182
+ }
183
+
184
+ /**
185
+ * Merge two Standards objects — base values are overridden by overrides.
186
+ * Only one level of nesting is merged (stack, doctrine sub-objects).
187
+ */
188
+ export function mergeStandards(base: Standards, overrides: Standards): Standards {
189
+ return {
190
+ version: overrides.version ?? base.version,
191
+ stack: overrides.stack || base.stack
192
+ ? { ...base.stack, ...overrides.stack }
193
+ : undefined,
194
+ doctrine: overrides.doctrine || base.doctrine
195
+ ? {
196
+ ...base.doctrine,
197
+ ...overrides.doctrine,
198
+ testing: overrides.doctrine?.testing || base.doctrine?.testing
199
+ ? { ...base.doctrine?.testing, ...overrides.doctrine?.testing }
200
+ : undefined,
201
+ architecture: overrides.doctrine?.architecture || base.doctrine?.architecture
202
+ ? { ...base.doctrine?.architecture, ...overrides.doctrine?.architecture }
203
+ : undefined,
204
+ code_style: overrides.doctrine?.code_style || base.doctrine?.code_style
205
+ ? { ...base.doctrine?.code_style, ...overrides.doctrine?.code_style }
206
+ : undefined,
207
+ }
208
+ : undefined,
209
+ };
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Doctrine serializer
214
+ //
215
+ // Renders a Standards object as a human-readable markdown-ish string
216
+ // suitable for injection into an AI worker's context alongside other doctrine.
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export function serializeStandardsAsDoctrine(standards: Standards): string {
220
+ const lines: string[] = ["# Engineering Standards (devory.standards.yml)", ""];
221
+
222
+ const { stack, doctrine } = standards;
223
+
224
+ if (stack) {
225
+ lines.push("## Stack");
226
+ if (stack.language) lines.push(`- Language: ${stack.language}`);
227
+ if (stack.framework) lines.push(`- Framework: ${stack.framework}`);
228
+ if (stack.database) lines.push(`- Database: ${stack.database}`);
229
+ lines.push("");
230
+ }
231
+
232
+ if (doctrine) {
233
+ if (doctrine.extends) {
234
+ lines.push(`## Baseline`);
235
+ lines.push(`Extends: ${doctrine.extends}`);
236
+ lines.push("");
237
+ }
238
+
239
+ if (doctrine.testing) {
240
+ lines.push("## Testing Standards");
241
+ const t = doctrine.testing;
242
+ if (t.require_unit !== undefined)
243
+ lines.push(`- Unit tests required: ${t.require_unit}`);
244
+ if (t.require_integration !== undefined)
245
+ lines.push(`- Integration tests required: ${t.require_integration}`);
246
+ if (t.coverage_threshold !== undefined)
247
+ lines.push(`- Coverage threshold: ${t.coverage_threshold}%`);
248
+ if (t.avoid_mocking?.length)
249
+ lines.push(`- Avoid mocking: ${t.avoid_mocking.join(", ")}`);
250
+ lines.push("");
251
+ }
252
+
253
+ if (doctrine.architecture) {
254
+ lines.push("## Architecture Standards");
255
+ const a = doctrine.architecture;
256
+ if (a.pattern) lines.push(`- Pattern: ${a.pattern}`);
257
+ if (a.max_file_lines !== undefined)
258
+ lines.push(`- Max file lines: ${a.max_file_lines}`);
259
+ if (a.no_circular_deps !== undefined)
260
+ lines.push(`- No circular dependencies: ${a.no_circular_deps}`);
261
+ lines.push("");
262
+ }
263
+
264
+ if (doctrine.code_style) {
265
+ lines.push("## Code Style Standards");
266
+ const cs = doctrine.code_style;
267
+ if (cs.no_any !== undefined) lines.push(`- No \`any\` type: ${cs.no_any}`);
268
+ if (cs.prefer_explicit_over_clever !== undefined)
269
+ lines.push(`- Prefer explicit over clever: ${cs.prefer_explicit_over_clever}`);
270
+ lines.push("");
271
+ }
272
+
273
+ if (doctrine.custom_rules?.length) {
274
+ lines.push("## Custom Rules");
275
+ for (const rule of doctrine.custom_rules) {
276
+ lines.push(`- ${rule}`);
277
+ }
278
+ lines.push("");
279
+ }
280
+ }
281
+
282
+ return lines.join("\n").trimEnd();
283
+ }