@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/dist/index.js +2903 -15
- package/package.json +1 -1
- package/src/defaults/generic.yml +18 -0
- package/src/defaults/typescript-nextjs.yml +29 -0
- package/src/defaults/typescript-node.yml +28 -0
- package/src/index.ts +27 -2
- package/src/license.ts +152 -0
- package/src/standards.ts +283 -0
package/package.json
CHANGED
|
@@ -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,
|
|
5
|
-
*
|
|
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
|
+
}
|
package/src/standards.ts
ADDED
|
@@ -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
|
+
}
|