@devory/core 0.1.1 → 0.3.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.
@@ -1,103 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
-
4
- export type FactoryRootSource =
5
- | "env:DEVORY_FACTORY_ROOT"
6
- | "env:FACTORY_ROOT"
7
- | "git-walk"
8
- | "cwd";
9
-
10
- export type FactoryMode = "local" | "hosted";
11
-
12
- export interface FactoryPaths {
13
- tasksDir: string;
14
- runsDir: string;
15
- artifactsDir: string;
16
- contextFile: string;
17
- }
18
-
19
- export interface FactoryEnvironment {
20
- root: string;
21
- source: FactoryRootSource;
22
- mode: FactoryMode;
23
- paths: FactoryPaths;
24
- }
25
-
26
- const FACTORY_MARKER = "FACTORY_CONTEXT.md";
27
-
28
- function trimEnv(value: string | undefined): string | null {
29
- if (typeof value !== "string") return null;
30
- const trimmed = value.trim();
31
- return trimmed === "" ? null : trimmed;
32
- }
33
-
34
- export function findFactoryContextDir(startDir: string): string | null {
35
- let current = path.resolve(startDir);
36
-
37
- while (true) {
38
- if (fs.existsSync(path.join(current, FACTORY_MARKER))) {
39
- return current;
40
- }
41
- const parent = path.dirname(current);
42
- if (parent === current) {
43
- return null;
44
- }
45
- current = parent;
46
- }
47
- }
48
-
49
- export function resolveFactoryRoot(startDir = process.cwd()): {
50
- root: string;
51
- source: FactoryRootSource;
52
- } {
53
- const explicit = trimEnv(process.env.DEVORY_FACTORY_ROOT);
54
- if (explicit) {
55
- return { root: explicit, source: "env:DEVORY_FACTORY_ROOT" };
56
- }
57
-
58
- const legacy = trimEnv(process.env.FACTORY_ROOT);
59
- if (legacy) {
60
- return { root: legacy, source: "env:FACTORY_ROOT" };
61
- }
62
-
63
- const walked = findFactoryContextDir(startDir);
64
- if (walked) {
65
- return { root: walked, source: "git-walk" };
66
- }
67
-
68
- return { root: path.resolve(startDir), source: "cwd" };
69
- }
70
-
71
- export function factoryPaths(root: string): FactoryPaths {
72
- return {
73
- tasksDir: path.join(root, "tasks"),
74
- runsDir: path.join(root, "runs"),
75
- artifactsDir: path.join(root, "artifacts"),
76
- contextFile: path.join(root, FACTORY_MARKER),
77
- };
78
- }
79
-
80
- export function resolveFactoryMode(env: NodeJS.ProcessEnv = process.env): FactoryMode {
81
- const explicitMode = trimEnv(env.DEVORY_FACTORY_MODE) ?? trimEnv(env.FACTORY_MODE);
82
- if (explicitMode === "hosted") return "hosted";
83
- if (explicitMode === "local") return "local";
84
-
85
- if (trimEnv(env.DEVORY_REMOTE_FACTORY_URL) || trimEnv(env.FACTORY_REMOTE_URL)) {
86
- return "hosted";
87
- }
88
-
89
- return "local";
90
- }
91
-
92
- export function resolveFactoryEnvironment(
93
- startDir = process.cwd(),
94
- env: NodeJS.ProcessEnv = process.env
95
- ): FactoryEnvironment {
96
- const { root, source } = resolveFactoryRoot(startDir);
97
- return {
98
- root,
99
- source,
100
- mode: resolveFactoryMode(env),
101
- paths: factoryPaths(root),
102
- };
103
- }
package/src/license.ts DELETED
@@ -1,152 +0,0 @@
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
- }
package/src/parse.test.ts DELETED
@@ -1,161 +0,0 @@
1
- /**
2
- * @devory/core — parseFrontmatter tests.
3
- *
4
- * Run from factory root: tsx --test packages/core/src/parse.test.ts
5
- */
6
-
7
- import { test, describe } from "node:test";
8
- import assert from "node:assert/strict";
9
- import { parseFrontmatter } from "./parse.js";
10
-
11
- // ── Basic parsing ─────────────────────────────────────────────────────────────
12
-
13
- describe("parseFrontmatter", () => {
14
- test("parses a minimal frontmatter block", () => {
15
- const content = `---
16
- id: factory-001
17
- title: My Task
18
- status: backlog
19
- ---
20
- Body text here.`;
21
- const { meta, body } = parseFrontmatter(content);
22
- assert.equal(meta.id, "factory-001");
23
- assert.equal(meta.title, "My Task");
24
- assert.equal(meta.status, "backlog");
25
- assert.ok(body.includes("Body text here."));
26
- });
27
-
28
- test("returns empty meta and full content when no frontmatter delimiter", () => {
29
- const content = "No frontmatter here.";
30
- const { meta, body } = parseFrontmatter(content);
31
- assert.deepEqual(meta, {});
32
- assert.equal(body, content);
33
- });
34
-
35
- test("returns empty meta when opening delimiter missing", () => {
36
- const content = "id: factory-001\n---\nbody";
37
- const { meta } = parseFrontmatter(content);
38
- assert.deepEqual(meta, {});
39
- });
40
-
41
- test("returns empty meta when closing delimiter missing", () => {
42
- const content = "---\nid: factory-001\nbody without close";
43
- const { meta } = parseFrontmatter(content);
44
- assert.deepEqual(meta, {});
45
- });
46
-
47
- test("parses string arrays from list items", () => {
48
- const content = `---
49
- depends_on:
50
- - factory-001
51
- - factory-002
52
- ---
53
- `;
54
- const { meta } = parseFrontmatter(content);
55
- assert.deepEqual(meta.depends_on, ["factory-001", "factory-002"]);
56
- });
57
-
58
- test("parses empty array from empty value", () => {
59
- const content = `---
60
- depends_on:
61
- ---
62
- `;
63
- const { meta } = parseFrontmatter(content);
64
- assert.deepEqual(meta.depends_on, []);
65
- });
66
-
67
- test("parses empty array from [] syntax", () => {
68
- const content = `---
69
- depends_on: []
70
- ---
71
- `;
72
- const { meta } = parseFrontmatter(content);
73
- assert.deepEqual(meta.depends_on, []);
74
- });
75
-
76
- test("body does not include frontmatter", () => {
77
- const content = `---
78
- id: factory-001
79
- ---
80
- ## Section
81
- Content here.`;
82
- const { body } = parseFrontmatter(content);
83
- assert.ok(!body.includes("id: factory-001"));
84
- assert.ok(body.includes("## Section"));
85
- });
86
-
87
- test("parses all standard task fields", () => {
88
- const content = `---
89
- id: factory-010
90
- title: Test Task
91
- project: ai-dev-factory
92
- repo: .
93
- branch: task/factory-010
94
- type: feature
95
- priority: high
96
- status: ready
97
- agent: fullstack-builder
98
- ---
99
- `;
100
- const { meta } = parseFrontmatter(content);
101
- assert.equal(meta.id, "factory-010");
102
- assert.equal(meta.title, "Test Task");
103
- assert.equal(meta.project, "ai-dev-factory");
104
- assert.equal(meta.type, "feature");
105
- assert.equal(meta.priority, "high");
106
- assert.equal(meta.agent, "fullstack-builder");
107
- });
108
-
109
- test("parses verification list", () => {
110
- const content = `---
111
- verification:
112
- - npm run test
113
- - npm run build
114
- ---
115
- `;
116
- const { meta } = parseFrontmatter(content);
117
- assert.deepEqual(meta.verification, ["npm run test", "npm run build"]);
118
- });
119
-
120
- test("handles hyphen-containing keys like depends_on", () => {
121
- const content = `---
122
- bundle-id: epic-auth
123
- ---
124
- `;
125
- const { meta } = parseFrontmatter(content);
126
- assert.equal(meta["bundle-id"], "epic-auth");
127
- });
128
-
129
- test("trims whitespace from scalar values", () => {
130
- const content = `---
131
- title: My Task
132
- status: backlog
133
- ---
134
- `;
135
- const { meta } = parseFrontmatter(content);
136
- assert.equal(meta.title, "My Task");
137
- assert.equal(meta.status, "backlog");
138
- });
139
-
140
- test("ignores list items before any key is set", () => {
141
- const content = `---
142
- - orphaned-item
143
- id: factory-001
144
- ---
145
- `;
146
- const { meta } = parseFrontmatter(content);
147
- assert.equal(meta.id, "factory-001");
148
- });
149
-
150
- test("empty content returns empty meta and empty body", () => {
151
- const { meta, body } = parseFrontmatter("");
152
- assert.deepEqual(meta, {});
153
- assert.equal(body, "");
154
- });
155
-
156
- test("content with only delimiter lines returns empty meta", () => {
157
- const content = "---\n---\n";
158
- const { meta } = parseFrontmatter(content);
159
- assert.deepEqual(meta, {});
160
- });
161
- });
package/src/parse.ts DELETED
@@ -1,103 +0,0 @@
1
- /**
2
- * @devory/core — shared frontmatter parsing utilities.
3
- *
4
- * Pure functions with no external dependencies.
5
- * Used by workers/lib, scripts, and apps/devory.
6
- */
7
-
8
- // ---------------------------------------------------------------------------
9
- // Types
10
- // ---------------------------------------------------------------------------
11
-
12
- export interface TaskMeta {
13
- id: string;
14
- title: string;
15
- project: string;
16
- repo: string;
17
- branch: string;
18
- type: string;
19
- priority: string;
20
- status: string;
21
- agent: string;
22
- /** Simulated execution outcome written by an agent after doing work. */
23
- execution_result?: string;
24
- depends_on: string[];
25
- files_likely_affected: string[];
26
- verification: string[];
27
-
28
- // Planner / parent-task fields (all optional)
29
- planner?: boolean;
30
- parent_task?: string;
31
- lane?: string;
32
- repo_area?: string;
33
- decomposition_hint?: string;
34
- required_capabilities?: string[];
35
- preferred_capabilities?: string[];
36
- disallowed_models?: string[];
37
- preferred_models?: string[];
38
- execution_profile?: string;
39
- pipeline?: string;
40
- context_intensity?: string;
41
- quality_priority?: string;
42
- speed_priority?: string;
43
- max_cost_tier?: string;
44
-
45
- // Bundle / epic fields (all optional)
46
- bundle_id?: string;
47
- bundle_title?: string;
48
- bundle_phase?: string;
49
-
50
- [key: string]: unknown;
51
- }
52
-
53
- export interface ParseResult {
54
- meta: Partial<TaskMeta>;
55
- body: string;
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Frontmatter parser
60
- // Handles simple YAML: scalar strings and flat string arrays ("- item").
61
- // Does not depend on any external package.
62
- // ---------------------------------------------------------------------------
63
-
64
- export function parseFrontmatter(content: string): ParseResult {
65
- const lines = content.split("\n");
66
-
67
- if (lines[0]?.trim() !== "---") {
68
- return { meta: {}, body: content };
69
- }
70
-
71
- const closeIdx = lines.indexOf("---", 1);
72
- if (closeIdx === -1) {
73
- return { meta: {}, body: content };
74
- }
75
-
76
- const yamlLines = lines.slice(1, closeIdx);
77
- const body = lines.slice(closeIdx + 1).join("\n");
78
- const meta: Partial<TaskMeta> = {};
79
- let currentKey = "";
80
-
81
- for (const line of yamlLines) {
82
- const listMatch = line.match(/^\s+-\s+(.*)/);
83
- const kvMatch = line.match(/^([\w_][\w_-]*):\s*(.*)/);
84
-
85
- if (listMatch && currentKey) {
86
- const arr = meta[currentKey];
87
- if (Array.isArray(arr)) {
88
- arr.push(listMatch[1].trim());
89
- }
90
- } else if (kvMatch) {
91
- currentKey = kvMatch[1];
92
- const val = kvMatch[2].trim();
93
-
94
- if (val === "" || val === "[]") {
95
- (meta as Record<string, unknown>)[currentKey] = [];
96
- } else {
97
- (meta as Record<string, unknown>)[currentKey] = val;
98
- }
99
- }
100
- }
101
-
102
- return { meta, body };
103
- }