@devory/core 0.0.1 → 0.1.0
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/README.md +41 -0
- package/dist/index.js +148 -0
- package/package.json +21 -4
- package/src/factory-environment.test.ts +99 -0
- package/src/factory-environment.ts +103 -0
- package/src/index.ts +22 -0
- package/src/parse.test.ts +161 -0
- package/src/parse.ts +103 -0
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @devory/core
|
|
2
|
+
|
|
3
|
+
Shared types, parsing utilities, and path configuration for [Devory](https://devory.ai).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @devory/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### Task parsing
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { parseFrontmatter } from '@devory/core'
|
|
17
|
+
|
|
18
|
+
const result = parseFrontmatter(fileContent)
|
|
19
|
+
// result.meta — typed TaskMeta fields
|
|
20
|
+
// result.body — markdown body after frontmatter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Factory environment
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { resolveFactoryEnvironment, factoryPaths } from '@devory/core'
|
|
27
|
+
|
|
28
|
+
const env = resolveFactoryEnvironment()
|
|
29
|
+
const paths = factoryPaths(env.root)
|
|
30
|
+
// paths.tasksDir, paths.runsDir, paths.artifactsDir, etc.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Types
|
|
34
|
+
|
|
35
|
+
- `TaskMeta` — frontmatter fields for a Devory task file
|
|
36
|
+
- `FactoryEnvironment` — resolved root, mode, and source
|
|
37
|
+
- `FactoryPaths` — all well-known directories in a factory workspace
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Node.js 18+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
factoryPaths: () => factoryPaths,
|
|
34
|
+
findFactoryContextDir: () => findFactoryContextDir,
|
|
35
|
+
parseFrontmatter: () => parseFrontmatter,
|
|
36
|
+
resolveFactoryEnvironment: () => resolveFactoryEnvironment,
|
|
37
|
+
resolveFactoryMode: () => resolveFactoryMode,
|
|
38
|
+
resolveFactoryRoot: () => resolveFactoryRoot
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/parse.ts
|
|
43
|
+
function parseFrontmatter(content) {
|
|
44
|
+
const lines = content.split("\n");
|
|
45
|
+
if (lines[0]?.trim() !== "---") {
|
|
46
|
+
return { meta: {}, body: content };
|
|
47
|
+
}
|
|
48
|
+
const closeIdx = lines.indexOf("---", 1);
|
|
49
|
+
if (closeIdx === -1) {
|
|
50
|
+
return { meta: {}, body: content };
|
|
51
|
+
}
|
|
52
|
+
const yamlLines = lines.slice(1, closeIdx);
|
|
53
|
+
const body = lines.slice(closeIdx + 1).join("\n");
|
|
54
|
+
const meta = {};
|
|
55
|
+
let currentKey = "";
|
|
56
|
+
for (const line of yamlLines) {
|
|
57
|
+
const listMatch = line.match(/^\s+-\s+(.*)/);
|
|
58
|
+
const kvMatch = line.match(/^([\w_][\w_-]*):\s*(.*)/);
|
|
59
|
+
if (listMatch && currentKey) {
|
|
60
|
+
const arr = meta[currentKey];
|
|
61
|
+
if (Array.isArray(arr)) {
|
|
62
|
+
arr.push(listMatch[1].trim());
|
|
63
|
+
}
|
|
64
|
+
} else if (kvMatch) {
|
|
65
|
+
currentKey = kvMatch[1];
|
|
66
|
+
const val = kvMatch[2].trim();
|
|
67
|
+
if (val === "" || val === "[]") {
|
|
68
|
+
meta[currentKey] = [];
|
|
69
|
+
} else {
|
|
70
|
+
meta[currentKey] = val;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { meta, body };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/factory-environment.ts
|
|
78
|
+
var fs = __toESM(require("fs"));
|
|
79
|
+
var path = __toESM(require("path"));
|
|
80
|
+
var FACTORY_MARKER = "FACTORY_CONTEXT.md";
|
|
81
|
+
function trimEnv(value) {
|
|
82
|
+
if (typeof value !== "string") return null;
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
return trimmed === "" ? null : trimmed;
|
|
85
|
+
}
|
|
86
|
+
function findFactoryContextDir(startDir) {
|
|
87
|
+
let current = path.resolve(startDir);
|
|
88
|
+
while (true) {
|
|
89
|
+
if (fs.existsSync(path.join(current, FACTORY_MARKER))) {
|
|
90
|
+
return current;
|
|
91
|
+
}
|
|
92
|
+
const parent = path.dirname(current);
|
|
93
|
+
if (parent === current) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function resolveFactoryRoot(startDir = process.cwd()) {
|
|
100
|
+
const explicit = trimEnv(process.env.DEVORY_FACTORY_ROOT);
|
|
101
|
+
if (explicit) {
|
|
102
|
+
return { root: explicit, source: "env:DEVORY_FACTORY_ROOT" };
|
|
103
|
+
}
|
|
104
|
+
const legacy = trimEnv(process.env.FACTORY_ROOT);
|
|
105
|
+
if (legacy) {
|
|
106
|
+
return { root: legacy, source: "env:FACTORY_ROOT" };
|
|
107
|
+
}
|
|
108
|
+
const walked = findFactoryContextDir(startDir);
|
|
109
|
+
if (walked) {
|
|
110
|
+
return { root: walked, source: "git-walk" };
|
|
111
|
+
}
|
|
112
|
+
return { root: path.resolve(startDir), source: "cwd" };
|
|
113
|
+
}
|
|
114
|
+
function factoryPaths(root) {
|
|
115
|
+
return {
|
|
116
|
+
tasksDir: path.join(root, "tasks"),
|
|
117
|
+
runsDir: path.join(root, "runs"),
|
|
118
|
+
artifactsDir: path.join(root, "artifacts"),
|
|
119
|
+
contextFile: path.join(root, FACTORY_MARKER)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function resolveFactoryMode(env = process.env) {
|
|
123
|
+
const explicitMode = trimEnv(env.DEVORY_FACTORY_MODE) ?? trimEnv(env.FACTORY_MODE);
|
|
124
|
+
if (explicitMode === "hosted") return "hosted";
|
|
125
|
+
if (explicitMode === "local") return "local";
|
|
126
|
+
if (trimEnv(env.DEVORY_REMOTE_FACTORY_URL) || trimEnv(env.FACTORY_REMOTE_URL)) {
|
|
127
|
+
return "hosted";
|
|
128
|
+
}
|
|
129
|
+
return "local";
|
|
130
|
+
}
|
|
131
|
+
function resolveFactoryEnvironment(startDir = process.cwd(), env = process.env) {
|
|
132
|
+
const { root, source } = resolveFactoryRoot(startDir);
|
|
133
|
+
return {
|
|
134
|
+
root,
|
|
135
|
+
source,
|
|
136
|
+
mode: resolveFactoryMode(env),
|
|
137
|
+
paths: factoryPaths(root)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
141
|
+
0 && (module.exports = {
|
|
142
|
+
factoryPaths,
|
|
143
|
+
findFactoryContextDir,
|
|
144
|
+
parseFrontmatter,
|
|
145
|
+
resolveFactoryEnvironment,
|
|
146
|
+
resolveFactoryMode,
|
|
147
|
+
resolveFactoryRoot
|
|
148
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devory/core",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared types, parsing utilities, and path configuration for AI Dev Factory",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=cjs",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"esbuild": "^0.20.0",
|
|
21
|
+
"tsx": "^4.19.2",
|
|
22
|
+
"typescript": "^5.7.3"
|
|
23
|
+
}
|
|
7
24
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
factoryPaths,
|
|
9
|
+
findFactoryContextDir,
|
|
10
|
+
resolveFactoryEnvironment,
|
|
11
|
+
resolveFactoryMode,
|
|
12
|
+
resolveFactoryRoot,
|
|
13
|
+
} from "./factory-environment.ts";
|
|
14
|
+
|
|
15
|
+
let tmpDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "factory-env-test-"));
|
|
19
|
+
delete process.env.DEVORY_FACTORY_ROOT;
|
|
20
|
+
delete process.env.FACTORY_ROOT;
|
|
21
|
+
delete process.env.DEVORY_FACTORY_MODE;
|
|
22
|
+
delete process.env.FACTORY_MODE;
|
|
23
|
+
delete process.env.DEVORY_REMOTE_FACTORY_URL;
|
|
24
|
+
delete process.env.FACTORY_REMOTE_URL;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
delete process.env.DEVORY_FACTORY_ROOT;
|
|
30
|
+
delete process.env.FACTORY_ROOT;
|
|
31
|
+
delete process.env.DEVORY_FACTORY_MODE;
|
|
32
|
+
delete process.env.FACTORY_MODE;
|
|
33
|
+
delete process.env.DEVORY_REMOTE_FACTORY_URL;
|
|
34
|
+
delete process.env.FACTORY_REMOTE_URL;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("findFactoryContextDir", () => {
|
|
38
|
+
test("returns the containing directory when marker exists", () => {
|
|
39
|
+
fs.writeFileSync(path.join(tmpDir, "FACTORY_CONTEXT.md"), "# context");
|
|
40
|
+
assert.equal(findFactoryContextDir(tmpDir), tmpDir);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("walks up parent directories to find the factory marker", () => {
|
|
44
|
+
fs.writeFileSync(path.join(tmpDir, "FACTORY_CONTEXT.md"), "# context");
|
|
45
|
+
const nested = path.join(tmpDir, "nested", "deep");
|
|
46
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
47
|
+
assert.equal(findFactoryContextDir(nested), tmpDir);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("resolveFactoryRoot", () => {
|
|
52
|
+
test("uses DEVORY_FACTORY_ROOT first", () => {
|
|
53
|
+
process.env.DEVORY_FACTORY_ROOT = "/explicit/path";
|
|
54
|
+
process.env.FACTORY_ROOT = "/legacy/path";
|
|
55
|
+
assert.deepEqual(resolveFactoryRoot(tmpDir), {
|
|
56
|
+
root: "/explicit/path",
|
|
57
|
+
source: "env:DEVORY_FACTORY_ROOT",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("walks to the factory marker when env vars are absent", () => {
|
|
62
|
+
fs.writeFileSync(path.join(tmpDir, "FACTORY_CONTEXT.md"), "# context");
|
|
63
|
+
const nested = path.join(tmpDir, "deep");
|
|
64
|
+
fs.mkdirSync(nested);
|
|
65
|
+
assert.deepEqual(resolveFactoryRoot(nested), {
|
|
66
|
+
root: tmpDir,
|
|
67
|
+
source: "git-walk",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("resolveFactoryMode", () => {
|
|
73
|
+
test("defaults to local", () => {
|
|
74
|
+
assert.equal(resolveFactoryMode(), "local");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("honors explicit hosted mode", () => {
|
|
78
|
+
process.env.DEVORY_FACTORY_MODE = "hosted";
|
|
79
|
+
assert.equal(resolveFactoryMode(), "hosted");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("treats remote url configuration as hosted mode", () => {
|
|
83
|
+
process.env.DEVORY_REMOTE_FACTORY_URL = "https://factory.example.com";
|
|
84
|
+
assert.equal(resolveFactoryMode(), "hosted");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("resolveFactoryEnvironment", () => {
|
|
89
|
+
test("returns root, mode, and derived paths together", () => {
|
|
90
|
+
fs.writeFileSync(path.join(tmpDir, "FACTORY_CONTEXT.md"), "# context");
|
|
91
|
+
process.env.DEVORY_FACTORY_MODE = "hosted";
|
|
92
|
+
|
|
93
|
+
const result = resolveFactoryEnvironment(tmpDir);
|
|
94
|
+
assert.equal(result.root, tmpDir);
|
|
95
|
+
assert.equal(result.source, "git-walk");
|
|
96
|
+
assert.equal(result.mode, "hosted");
|
|
97
|
+
assert.deepEqual(result.paths, factoryPaths(tmpDir));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
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/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @devory/core — public API
|
|
3
|
+
*
|
|
4
|
+
* Shared types, parsing utilities, and path configuration
|
|
5
|
+
* for the AI Dev Factory monorepo.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { parseFrontmatter } from "./parse.ts";
|
|
9
|
+
export type { TaskMeta, ParseResult } from "./parse.ts";
|
|
10
|
+
export {
|
|
11
|
+
factoryPaths,
|
|
12
|
+
findFactoryContextDir,
|
|
13
|
+
resolveFactoryEnvironment,
|
|
14
|
+
resolveFactoryMode,
|
|
15
|
+
resolveFactoryRoot,
|
|
16
|
+
} from "./factory-environment.ts";
|
|
17
|
+
export type {
|
|
18
|
+
FactoryEnvironment,
|
|
19
|
+
FactoryMode,
|
|
20
|
+
FactoryPaths,
|
|
21
|
+
FactoryRootSource,
|
|
22
|
+
} from "./factory-environment.ts";
|
|
@@ -0,0 +1,161 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|