project-iris 0.0.8 → 0.0.11
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 +294 -264
- package/dist/bridge/agent-runner.js +190 -0
- package/dist/bridge/connector-factory.js +4 -0
- package/dist/bridge/connectors/in-process-connector.js +29 -0
- package/dist/bridge/filesystem-connector.js +5 -0
- package/dist/cli.js +10 -2
- package/dist/commands/ask.js +150 -23
- package/dist/commands/bridge.js +8 -0
- package/dist/commands/flow.js +301 -0
- package/dist/commands/framework.js +273 -0
- package/dist/commands/generate.js +59 -0
- package/dist/commands/install.js +72 -29
- package/dist/commands/pack.js +7 -1
- package/dist/commands/run.js +195 -13
- package/dist/commands/status.js +9 -0
- package/dist/commands/uninstall.js +3 -1
- package/dist/commands/use.js +20 -0
- package/dist/commands/validate.js +80 -65
- package/dist/framework/framework-loader.js +97 -0
- package/dist/framework/framework-paths.js +48 -0
- package/dist/framework/framework-types.js +15 -0
- package/dist/iris/artifacts/config.js +68 -0
- package/dist/iris/artifacts/generator.js +88 -0
- package/dist/iris/artifacts/types.js +1 -0
- package/dist/iris/bundle.js +44 -0
- package/dist/iris/doctrine/collector.js +124 -0
- package/dist/iris/fixer.js +28 -22
- package/dist/iris/flows/manifest.js +124 -0
- package/dist/iris/framework-context.js +49 -0
- package/dist/iris/framework-manager.js +215 -0
- package/dist/iris/fs/atomic.js +22 -0
- package/dist/iris/importers/index.js +9 -0
- package/dist/iris/importers/types.js +8 -0
- package/dist/iris/importers/writer.js +139 -0
- package/dist/iris/installer.js +105 -40
- package/dist/iris/interactive/env.js +21 -0
- package/dist/iris/interactive/intent-interview.js +345 -0
- package/dist/iris/interactive/intent-schema.js +28 -0
- package/dist/iris/interactive/interview-io.js +22 -0
- package/dist/iris/interview/config.js +71 -0
- package/dist/iris/interview/types.js +16 -0
- package/dist/iris/interview/utils.js +38 -0
- package/dist/iris/packer.js +69 -47
- package/dist/iris/parsers/unit-parser.js +43 -0
- package/dist/iris/paths.js +18 -0
- package/dist/iris/policy.js +122 -17
- package/dist/iris/proc.js +56 -0
- package/dist/iris/resolver.js +3 -0
- package/dist/iris/routes.js +180 -11
- package/dist/iris/run-state.js +3 -0
- package/dist/iris/state.js +37 -9
- package/dist/iris/templates.js +70 -0
- package/dist/iris/tmp.js +24 -0
- package/dist/iris/uninstaller.js +24 -9
- package/dist/iris/utils/interpolate.js +42 -0
- package/dist/iris/validator.js +72 -10
- package/dist/iris/workflow/config.js +51 -0
- package/dist/iris/workflow/engine.js +129 -0
- package/dist/iris/workflow/steps.js +448 -0
- package/dist/iris/workflow/types.js +1 -0
- package/dist/utils/logo.js +17 -0
- package/dist/workflows/intent-inception.js +87 -65
- package/package.json +8 -6
- package/src/iris_bundle/.iris/aidlc/README.md +0 -16
- package/src/iris_bundle/.iris/aidlc/agents/iris-construction-agent.md +0 -35
- package/src/iris_bundle/.iris/aidlc/agents/iris-inception-agent.md +0 -30
- package/src/iris_bundle/.iris/aidlc/agents/iris-master-agent.md +0 -35
- package/src/iris_bundle/.iris/aidlc/agents/iris-operations-agent.md +0 -29
- package/src/iris_bundle/.iris/aidlc/commands/iris-construction-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-inception-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-master-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-operations-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/context/context-map.md +0 -25
- package/src/iris_bundle/.iris/aidlc/context/exclusion-rules.md +0 -13
- package/src/iris_bundle/.iris/aidlc/context/load-order.md +0 -25
- package/src/iris_bundle/.iris/aidlc/memory/intent-rules.md +0 -9
- package/src/iris_bundle/.iris/aidlc/memory/log-rules.md +0 -5
- package/src/iris_bundle/.iris/aidlc/memory/memory-bank.yaml +0 -39
- package/src/iris_bundle/.iris/aidlc/memory/unit-rules.md +0 -9
- package/src/iris_bundle/.iris/aidlc/quick-start.md +0 -24
- package/src/iris_bundle/.iris/aidlc/skills/execution/implementation.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/execution/refactoring.md +0 -13
- package/src/iris_bundle/.iris/aidlc/skills/execution/scaffold-generation.md +0 -15
- package/src/iris_bundle/.iris/aidlc/skills/governance/escalation.md +0 -13
- package/src/iris_bundle/.iris/aidlc/skills/governance/quality-gates.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/governance/stop-conditions.md +0 -11
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/decomposition.md +0 -23
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/risk-analysis.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/verification.md +0 -21
- package/src/iris_bundle/.iris/aidlc/standards/artifacts-registry.md +0 -38
- package/src/iris_bundle/.iris/aidlc/standards/decision-logging.md +0 -16
- package/src/iris_bundle/.iris/aidlc/standards/doctrine-structure.md +0 -31
- package/src/iris_bundle/.iris/aidlc/standards/documentation-rules.md +0 -15
- package/src/iris_bundle/.iris/aidlc/standards/file-structure.md +0 -21
- package/src/iris_bundle/.iris/aidlc/standards/naming-conventions.md +0 -18
- package/src/iris_bundle/.iris/aidlc/standards/phases-and-gates.md +0 -25
- package/src/iris_bundle/.iris/aidlc/standards/routes-and-routing.md +0 -35
- package/src/iris_bundle/.iris/aidlc/standards/tool-wrappers.md +0 -32
- package/src/iris_bundle/.iris/aidlc/templates/bolt.md +0 -23
- package/src/iris_bundle/.iris/aidlc/templates/doctrine-doc-template.md +0 -33
- package/src/iris_bundle/.iris/aidlc/templates/intent.md +0 -23
- package/src/iris_bundle/.iris/aidlc/templates/log.md +0 -24
- package/src/iris_bundle/.iris/aidlc/templates/review.md +0 -21
- package/src/iris_bundle/.iris/aidlc/templates/unit.md +0 -31
- package/src/iris_bundle/.iris/aidlc/validation/failure-modes.md +0 -16
- package/src/iris_bundle/.iris/aidlc/validation/phase-preconditions.md +0 -21
- package/src/iris_bundle/.iris/aidlc/validation/quality-checklist.md +0 -20
- package/src/iris_bundle/.iris/policy.yaml +0 -27
- package/src/iris_bundle/.iris/routes.yaml +0 -98
- package/src/iris_bundle/.iris/state.yaml +0 -7
- package/src/iris_bundle/.iris/tools/claude/.claude/claude.md +0 -9
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/compare-specs.md +0 -203
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-construction-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-inception-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-master-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-operations-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/codex/AGENTS.md +0 -15
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-construction-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-inception-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-master-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-operations-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-construction-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-inception-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-master-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-operations-agent.toml +0 -29
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { FrameworkError } from './framework-types.js';
|
|
5
|
+
import { resolveFrameworkRoot } from './framework-paths.js';
|
|
6
|
+
export async function loadFramework(input, options) {
|
|
7
|
+
const rootDir = resolveFrameworkRoot(input, options);
|
|
8
|
+
// 1. Validate Root Directory
|
|
9
|
+
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
|
|
10
|
+
throw new FrameworkError('NOT_FOUND', rootDir, `Framework directory not found: ${rootDir}`, 'Expected a valid directory at the resolved path.');
|
|
11
|
+
}
|
|
12
|
+
// 2. Locate Manifest
|
|
13
|
+
let manifestPath = path.join(rootDir, 'framework.yaml');
|
|
14
|
+
if (!fs.existsSync(manifestPath)) {
|
|
15
|
+
manifestPath = path.join(rootDir, 'framework.yml');
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(manifestPath)) {
|
|
18
|
+
throw new FrameworkError('MISSING_MANIFEST', rootDir, `No framework.yaml (or .yml) found in ${rootDir}`);
|
|
19
|
+
}
|
|
20
|
+
// 3. Parse Manifest
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
24
|
+
raw = yaml.load(content);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
throw new FrameworkError('INVALID_YAML', manifestPath, `Failed to parse framework manifest: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
// 4. Validate Schema
|
|
30
|
+
if (!raw || typeof raw !== 'object') {
|
|
31
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Manifest must be an object');
|
|
32
|
+
}
|
|
33
|
+
if (typeof raw.name !== 'string' || !raw.name) {
|
|
34
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Missing required field: name');
|
|
35
|
+
}
|
|
36
|
+
if (typeof raw.version !== 'string' || !raw.version) {
|
|
37
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Missing required field: version');
|
|
38
|
+
}
|
|
39
|
+
if (raw.id && typeof raw.id !== 'string') {
|
|
40
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Field "id" must be a string');
|
|
41
|
+
}
|
|
42
|
+
// Loose ID validation
|
|
43
|
+
if (raw.id && !/^[a-z0-9-_/]+$/.test(raw.id)) {
|
|
44
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Field "id" contains invalid characters (allowed: [a-z0-9-_/])');
|
|
45
|
+
}
|
|
46
|
+
if (raw.extends && typeof raw.extends !== 'string' && !Array.isArray(raw.extends)) {
|
|
47
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Field "extends" must be a string or array of strings');
|
|
48
|
+
}
|
|
49
|
+
if (raw.overlays && (!Array.isArray(raw.overlays) || raw.overlays.some((o) => typeof o !== 'string'))) {
|
|
50
|
+
throw new FrameworkError('INVALID_SCHEMA', manifestPath, 'Field "overlays" must be an array of strings');
|
|
51
|
+
}
|
|
52
|
+
const manifest = {
|
|
53
|
+
name: raw.name,
|
|
54
|
+
version: raw.version,
|
|
55
|
+
id: raw.id,
|
|
56
|
+
extends: raw.extends,
|
|
57
|
+
overlays: raw.overlays,
|
|
58
|
+
templatesDir: raw.templatesDir
|
|
59
|
+
};
|
|
60
|
+
// 5. Discover Files
|
|
61
|
+
const files = {
|
|
62
|
+
manifest: manifestPath
|
|
63
|
+
};
|
|
64
|
+
const tryFile = (name) => {
|
|
65
|
+
const p = path.join(rootDir, name);
|
|
66
|
+
return fs.existsSync(p) ? p : undefined;
|
|
67
|
+
};
|
|
68
|
+
files.policy = tryFile('policy.yaml');
|
|
69
|
+
files.routes = tryFile('routes.yaml');
|
|
70
|
+
files.artifacts = tryFile('artifacts.yaml');
|
|
71
|
+
files.interview = tryFile('interview.yaml');
|
|
72
|
+
files.pack = tryFile('pack.yaml');
|
|
73
|
+
// 6. Handle Templates
|
|
74
|
+
if (manifest.templatesDir) {
|
|
75
|
+
const tplPath = path.resolve(rootDir, manifest.templatesDir);
|
|
76
|
+
if (!fs.existsSync(tplPath) || !fs.statSync(tplPath).isDirectory()) {
|
|
77
|
+
throw new FrameworkError('MISSING_FILE', tplPath, `Configured templatesDir not found: ${manifest.templatesDir}`);
|
|
78
|
+
}
|
|
79
|
+
files.templates = tplPath;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const defaultTpl = path.join(rootDir, 'templates');
|
|
83
|
+
if (fs.existsSync(defaultTpl) && fs.statSync(defaultTpl).isDirectory()) {
|
|
84
|
+
files.templates = defaultTpl;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
manifest,
|
|
89
|
+
source: {
|
|
90
|
+
kind: 'local', // Step 1 assumption
|
|
91
|
+
path: rootDir,
|
|
92
|
+
id: input
|
|
93
|
+
},
|
|
94
|
+
files,
|
|
95
|
+
rootDir
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a framework input (name or path) to an absolute directory path.
|
|
8
|
+
*
|
|
9
|
+
* Heuristics:
|
|
10
|
+
* - If input implies a path (starts with ./ or /, is absolute, contains separator),
|
|
11
|
+
* it is resolved relative to repoRoot (unless absolute).
|
|
12
|
+
* - Otherwise, it is treated as a framework name/ID.
|
|
13
|
+
* 1. Check .iris/frameworks/<name> in repo.
|
|
14
|
+
* 2. Check bundled frameworks.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveFrameworkRoot(input, options) {
|
|
17
|
+
const { repoRoot } = options;
|
|
18
|
+
// Strict heuristics to distinguish path from name
|
|
19
|
+
const isPath = input.startsWith('.') ||
|
|
20
|
+
input.startsWith('/') ||
|
|
21
|
+
input.startsWith('\\') || // Windows explicit relative
|
|
22
|
+
path.isAbsolute(input);
|
|
23
|
+
if (isPath) {
|
|
24
|
+
return path.resolve(repoRoot, input);
|
|
25
|
+
}
|
|
26
|
+
// Check Repo Local
|
|
27
|
+
const localPath = path.resolve(repoRoot, '.iris/frameworks', input);
|
|
28
|
+
if (fs.existsSync(localPath)) {
|
|
29
|
+
return localPath;
|
|
30
|
+
}
|
|
31
|
+
// Check Bundled
|
|
32
|
+
// Bundle structure assumption:
|
|
33
|
+
// dist/framework/framework-paths.js -> ... -> src/iris_bundle/frameworks/
|
|
34
|
+
// actually, in build: dist/iris_bundle/frameworks/<name>
|
|
35
|
+
// In dev (ts-node): src/framework/framework-paths.ts -> ../iris_bundle/frameworks
|
|
36
|
+
// Try reliable bundle resolution
|
|
37
|
+
const candidates = [
|
|
38
|
+
path.resolve(__dirname, '../../dist/iris_bundle/frameworks', input), // from dist/framework/
|
|
39
|
+
path.resolve(__dirname, '../iris_bundle/frameworks', input), // from src/framework/
|
|
40
|
+
path.resolve(__dirname, '../../src/iris_bundle/frameworks', input) // from src/framework/ (alt)
|
|
41
|
+
];
|
|
42
|
+
for (const c of candidates) {
|
|
43
|
+
if (fs.existsSync(c))
|
|
44
|
+
return c;
|
|
45
|
+
}
|
|
46
|
+
// Fallback to local path so error message shows expected repo location
|
|
47
|
+
return localPath;
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for IRIS Framework abstraction.
|
|
3
|
+
*/
|
|
4
|
+
export class FrameworkError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
path;
|
|
7
|
+
hint;
|
|
8
|
+
constructor(code, path, message, hint) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.hint = hint;
|
|
13
|
+
this.name = 'FrameworkError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
// Default Artifacts: Summary + User Stories
|
|
5
|
+
export const DEFAULT_ARTIFACTS_CONFIG = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
artifacts: [
|
|
8
|
+
{
|
|
9
|
+
id: "summary",
|
|
10
|
+
type: "summary",
|
|
11
|
+
mode: "template",
|
|
12
|
+
output: {
|
|
13
|
+
template: "# Summary\n\n**Goal**: {{goal}}\n\n**User**: {{userType}}\n**Phase**: {{projectPhase}}\n\n**Success Criteria**:\n{{successCriteria}}\n"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "user-stories",
|
|
18
|
+
type: "stories",
|
|
19
|
+
mode: "template",
|
|
20
|
+
required: false, // Don't fail if fields missing, just skip or partial?
|
|
21
|
+
// Wait, default should be robust.
|
|
22
|
+
// If fields missing, template will fail.
|
|
23
|
+
// Let's require standard fields.
|
|
24
|
+
input: { from: ["goal", "userType"] },
|
|
25
|
+
output: {
|
|
26
|
+
template: "As a {{userType}}, I want to {{goal}}, so that [Value Placeholders]"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
export function loadEffectiveArtifactsConfig(framework) {
|
|
32
|
+
if (!framework || !framework.files.artifacts || !fs.existsSync(framework.files.artifacts)) {
|
|
33
|
+
return DEFAULT_ARTIFACTS_CONFIG;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(framework.files.artifacts, "utf8");
|
|
37
|
+
const raw = yaml.load(content);
|
|
38
|
+
if (raw.schemaVersion !== 1) {
|
|
39
|
+
console.error(kleur.yellow(`IRIS_WARNING: Unsupported artifacts schema version ${raw.schemaVersion}. Using default.`));
|
|
40
|
+
return DEFAULT_ARTIFACTS_CONFIG;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(raw.artifacts)) {
|
|
43
|
+
console.error(kleur.yellow(`IRIS_WARNING: Invalid artifacts.yaml (missing 'artifacts' array). Using default.`));
|
|
44
|
+
return DEFAULT_ARTIFACTS_CONFIG;
|
|
45
|
+
}
|
|
46
|
+
// Validate fields against KNOWN_INTENT_FIELDS
|
|
47
|
+
for (const art of raw.artifacts) {
|
|
48
|
+
if (art.input?.from) {
|
|
49
|
+
for (const field of art.input.from) {
|
|
50
|
+
// Check against KNOWN fields (or allow unknown if we trust framework author? Plan said "Config Error")
|
|
51
|
+
// Actually, plan says "Check missing input fields in draft".
|
|
52
|
+
// But here we validate the CONFIG itself referencing known variables.
|
|
53
|
+
// KNOWN_INTENT_FIELDS includes common ones.
|
|
54
|
+
// Let's be lenient on config validation for now to support custom aliases,
|
|
55
|
+
// but strict on generation (if draft misses it).
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: raw.schemaVersion,
|
|
61
|
+
artifacts: raw.artifacts
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error(kleur.yellow(`IRIS_WARNING: Failed to parse artifacts.yaml: ${e.message}. Using default.`));
|
|
66
|
+
return DEFAULT_ARTIFACTS_CONFIG;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getByPath, isMissing } from "../interview/utils.js";
|
|
2
|
+
import { interpolateTokens } from "../utils/interpolate.js";
|
|
3
|
+
// Error Codes
|
|
4
|
+
export const ERRORS = {
|
|
5
|
+
MODE_NOT_SUPPORTED: "ARTIFACT_MODE_NOT_SUPPORTED",
|
|
6
|
+
INPUT_MISSING: "ARTIFACT_INPUT_MISSING",
|
|
7
|
+
TEMPLATE_VAR_MISSING: "ARTIFACT_TEMPLATE_MISSING_VAR",
|
|
8
|
+
UNSUPPORTED_ARRAY: "ARTIFACT_TEMPLATE_UNSUPPORTED_ARRAY_TYPE"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Renders a simple template with {{var}} substitution.
|
|
12
|
+
* Supports dot-paths.
|
|
13
|
+
*/
|
|
14
|
+
function renderTemplate(template, context, artifactId) {
|
|
15
|
+
// Strict regex: only alphanumeric, underscore, dot.
|
|
16
|
+
// Trims whitespace inside braces.
|
|
17
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (match, path) => {
|
|
18
|
+
const key = path.trim();
|
|
19
|
+
// Safety: Prevent prototype pollution access
|
|
20
|
+
if (key.includes('__proto__') || key.includes('constructor') || key.includes('prototype')) {
|
|
21
|
+
throw new Error(`${ERRORS.TEMPLATE_VAR_MISSING}: Unsafe variable path '${key}' in artifact '${artifactId}'`);
|
|
22
|
+
}
|
|
23
|
+
const value = getByPath(context, key);
|
|
24
|
+
if (value === undefined || value === null) {
|
|
25
|
+
throw new Error(`${ERRORS.TEMPLATE_VAR_MISSING}: Artifact '${artifactId}' requires variable '${key}'`);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
// Check for objects (non-primitives)
|
|
29
|
+
if (value.some(v => typeof v === 'object' && v !== null)) {
|
|
30
|
+
throw new Error(`${ERRORS.UNSUPPORTED_ARRAY}: Artifact '${artifactId}' variable '${key}' contains objects (unsupported for joining).`);
|
|
31
|
+
}
|
|
32
|
+
return value.join('\n');
|
|
33
|
+
}
|
|
34
|
+
return String(value);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function generateArtifacts(draft, config, frameworkResolution, intentId = null) {
|
|
38
|
+
const artifacts = [];
|
|
39
|
+
for (const def of config.artifacts) {
|
|
40
|
+
// 1. Mode Check
|
|
41
|
+
if (def.mode === 'llm') {
|
|
42
|
+
throw new Error(`${ERRORS.MODE_NOT_SUPPORTED}: LLM mode not supported yet (Artifact: ${def.id})`);
|
|
43
|
+
}
|
|
44
|
+
// 2. Input Validation (Declared requirements)
|
|
45
|
+
if (def.input?.from) {
|
|
46
|
+
const missing = def.input.from.filter(f => isMissing(getByPath(draft, f)));
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
throw new Error(`${ERRORS.INPUT_MISSING}: Artifact '${def.id}' requires missing fields: ${missing.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. Transformation (Template)
|
|
52
|
+
if (def.output.template) {
|
|
53
|
+
const content = renderTemplate(def.output.template, draft, def.id);
|
|
54
|
+
// Interpolate 'out' path if specified
|
|
55
|
+
let outPath;
|
|
56
|
+
if (def.out) {
|
|
57
|
+
try {
|
|
58
|
+
outPath = interpolateTokens(def.out, { intentId: intentId || undefined });
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
throw new Error(`${ERRORS.TEMPLATE_VAR_MISSING}: Path interpolation failed for artifact '${def.id}': ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
artifacts.push({
|
|
65
|
+
id: def.id,
|
|
66
|
+
type: def.type,
|
|
67
|
+
content,
|
|
68
|
+
path: outPath
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const isFramework = frameworkResolution && frameworkResolution.files.artifacts;
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
framework: {
|
|
76
|
+
id: frameworkResolution?.manifest.id || "default",
|
|
77
|
+
version: frameworkResolution?.manifest.version || null
|
|
78
|
+
},
|
|
79
|
+
intentId: intentId,
|
|
80
|
+
artifacts,
|
|
81
|
+
artifactsSource: {
|
|
82
|
+
kind: isFramework ? "framework" : "default",
|
|
83
|
+
path: isFramework ? frameworkResolution.files.artifacts : null,
|
|
84
|
+
activeFrameworkId: frameworkResolution?.manifest.id || null
|
|
85
|
+
},
|
|
86
|
+
createdAt: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the root of the IRIS Bundle (src/iris_bundle or dist/iris_bundle)
|
|
8
|
+
* independent of CWD.
|
|
9
|
+
*/
|
|
10
|
+
export function getBundleRoot() {
|
|
11
|
+
// 1. Production / Dist layout
|
|
12
|
+
// __dirname is usually dist/iris or similar. Bundle in dist/iris_bundle
|
|
13
|
+
// Check sibling of 'dist' if we are in dist/iris
|
|
14
|
+
// Attempt 1: Sibling 'iris_bundle' in expected location relative to THIS file
|
|
15
|
+
// Example: .../dist/iris/bundle.js -> .../dist/iris_bundle
|
|
16
|
+
// Or: .../src/iris/bundle.ts -> .../src/iris_bundle
|
|
17
|
+
const candidates = [
|
|
18
|
+
path.resolve(__dirname, "../iris_bundle"), // Typical sibling in src/ or dist/
|
|
19
|
+
path.resolve(__dirname, "../../src/iris_bundle"), // Dev fallback if running from dist but source present
|
|
20
|
+
path.resolve(__dirname, "../../iris_bundle") // Another dist layout possibility
|
|
21
|
+
];
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
if (fs.existsSync(candidate) && fs.existsSync(path.join(candidate, ".iris"))) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Fallback/Throw?
|
|
28
|
+
// In dev environment, if src/iris_bundle exists relative to root?
|
|
29
|
+
throw new Error(`Could not resolve IRIS bundle root. Searched: ${candidates.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
export function getBundledFlowsDir() {
|
|
32
|
+
return path.join(getBundleRoot(), ".iris/flows");
|
|
33
|
+
}
|
|
34
|
+
export function getBundledFrameworksDir() {
|
|
35
|
+
return path.join(getBundleRoot(), "frameworks");
|
|
36
|
+
}
|
|
37
|
+
export function listBundledFlows() {
|
|
38
|
+
const flowsDir = getBundledFlowsDir();
|
|
39
|
+
if (!fs.existsSync(flowsDir))
|
|
40
|
+
return [];
|
|
41
|
+
return fs.readdirSync(flowsDir, { withFileTypes: true })
|
|
42
|
+
.filter(ent => ent.isDirectory())
|
|
43
|
+
.map(ent => ent.name);
|
|
44
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
import { loadFlowManifest, getFlowDoctrineRoot } from "../flows/manifest.js";
|
|
5
|
+
// --- Cache ---
|
|
6
|
+
const manifestCache = new Map();
|
|
7
|
+
// --- Internal Helpers ---
|
|
8
|
+
function getManifestCached(root, flowId) {
|
|
9
|
+
const key = `${root}:${flowId}`;
|
|
10
|
+
if (manifestCache.has(key)) {
|
|
11
|
+
const cached = manifestCache.get(key);
|
|
12
|
+
if (!cached)
|
|
13
|
+
throw new Error(`Flow '${flowId}' manifest previously failed to load.`);
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const m = loadFlowManifest(root, flowId);
|
|
18
|
+
manifestCache.set(key, m);
|
|
19
|
+
return m;
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
manifestCache.set(key, null); // Negative cache? Or just throw.
|
|
23
|
+
// If manifest is required (active flow), we should throw.
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Recursively walks a directory and returns map of relative -> absolute paths.
|
|
29
|
+
*/
|
|
30
|
+
function walkDoctrineDir(dir, relativePrefix = "", index) {
|
|
31
|
+
if (!fs.existsSync(dir))
|
|
32
|
+
return;
|
|
33
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const relativePath = path.join(relativePrefix, entry.name);
|
|
36
|
+
const absolutePath = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
walkDoctrineDir(absolutePath, relativePath, index);
|
|
39
|
+
}
|
|
40
|
+
else if (entry.isFile()) {
|
|
41
|
+
// Normalize separators to forward slash for consistency in keys
|
|
42
|
+
const key = relativePath.split(path.sep).join("/");
|
|
43
|
+
index.set(key, absolutePath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// --- Public API ---
|
|
48
|
+
/**
|
|
49
|
+
* Collects all doctrine files, overlaying Flow doctrine over Base doctrine.
|
|
50
|
+
* Returns a Map<relative, absolute>.
|
|
51
|
+
*/
|
|
52
|
+
export function collectDoctrineIndex(root, activeFlowId) {
|
|
53
|
+
const index = new Map();
|
|
54
|
+
let overrides = 0;
|
|
55
|
+
// 1. Base Doctrine
|
|
56
|
+
const baseDoctrine = path.join(root, ".iris/aidlc");
|
|
57
|
+
walkDoctrineDir(baseDoctrine, "", index); // Populates index
|
|
58
|
+
// 2. Flow Doctrine (Overlay)
|
|
59
|
+
if (activeFlowId) {
|
|
60
|
+
try {
|
|
61
|
+
const manifest = getManifestCached(root, activeFlowId);
|
|
62
|
+
const flowDoctrine = getFlowDoctrineRoot(root, activeFlowId, manifest);
|
|
63
|
+
if (fs.existsSync(flowDoctrine)) {
|
|
64
|
+
// We walk flow doctrine and overwrite base keys
|
|
65
|
+
const flowIndex = new Map();
|
|
66
|
+
walkDoctrineDir(flowDoctrine, "", flowIndex);
|
|
67
|
+
for (const [key, absPath] of flowIndex) {
|
|
68
|
+
if (index.has(key))
|
|
69
|
+
overrides++;
|
|
70
|
+
index.set(key, absPath);
|
|
71
|
+
}
|
|
72
|
+
// Debug log (if verbose env var, or just console.debug if we had a logger)
|
|
73
|
+
// console.debug(`[Doctrine] Active Flow: ${activeFlowId}. Overrides: ${overrides}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
// Strict error if flow is active but manifest fails
|
|
78
|
+
console.error(kleur.red(`Failed to load manifest for active flow '${activeFlowId}': ${e.message}`));
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return index;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Collect all effective doctrine files as ABSOLUTE paths.
|
|
86
|
+
* Useful for tools that need to iterate all doctrine (like Packer).
|
|
87
|
+
*/
|
|
88
|
+
export function collectDoctrineFiles(root, activeFlowId) {
|
|
89
|
+
const index = collectDoctrineIndex(root, activeFlowId);
|
|
90
|
+
return Array.from(index.values());
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a specific doctrine file by its DOCTRINE-RELATIVE path.
|
|
94
|
+
*
|
|
95
|
+
* @param root Repo root
|
|
96
|
+
* @param doctrineRelPath Relative path within doctrine dir (e.g. "templates/foo.md")
|
|
97
|
+
* @param activeFlowId Active flow ID (optional)
|
|
98
|
+
* @returns Absolute path to the resolved file, or undefined if not found in either flow or base.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveDoctrinePath(root, doctrineRelPath, activeFlowId) {
|
|
101
|
+
// 1. Check Flow Override
|
|
102
|
+
if (activeFlowId) {
|
|
103
|
+
try {
|
|
104
|
+
const manifest = getManifestCached(root, activeFlowId);
|
|
105
|
+
// Default doctrine dir is 'doctrine' if not specified
|
|
106
|
+
const flowDoctrineDir = manifest.entrypoints?.doctrine_dir || "doctrine";
|
|
107
|
+
// Construct absolute path to flow doctrine file
|
|
108
|
+
const flowAbsPath = path.join(root, ".iris/flows", activeFlowId, flowDoctrineDir, doctrineRelPath);
|
|
109
|
+
if (fs.existsSync(flowAbsPath)) {
|
|
110
|
+
return flowAbsPath;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
// Let it throw to caller.
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// 2. Check Base Doctrine (.iris/aidlc)
|
|
119
|
+
const baseAbsPath = path.join(root, ".iris/aidlc", doctrineRelPath);
|
|
120
|
+
if (fs.existsSync(baseAbsPath)) {
|
|
121
|
+
return baseAbsPath;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
package/dist/iris/fixer.js
CHANGED
|
@@ -2,12 +2,25 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import kleur from "kleur";
|
|
4
4
|
import { repoRoot, ensureDir } from "../lib.js";
|
|
5
|
+
import { resolveDoctrinePath } from "./doctrine/collector.js";
|
|
6
|
+
import { loadState } from "./state.js";
|
|
7
|
+
function toDoctrineRelativeTemplatePath(t) {
|
|
8
|
+
const prefix = ".iris/aidlc/";
|
|
9
|
+
if (t.startsWith(prefix))
|
|
10
|
+
return t.substring(prefix.length);
|
|
11
|
+
// If someone already passed doctrine-relative, allow it (but ensure no leading slash if needed, usually robust)
|
|
12
|
+
if (!t.startsWith(".iris/"))
|
|
13
|
+
return t;
|
|
14
|
+
return t; // Unknown path type, return as-is
|
|
15
|
+
}
|
|
5
16
|
export function collectFixPlan(result) {
|
|
6
17
|
const plan = {
|
|
7
18
|
actions: [],
|
|
8
19
|
skipped: []
|
|
9
20
|
};
|
|
10
21
|
const root = repoRoot();
|
|
22
|
+
const state = loadState();
|
|
23
|
+
const activeFlowId = state.active.flow;
|
|
11
24
|
for (const error of result.errors) {
|
|
12
25
|
if (error.code !== "MISSING_ARTIFACT") {
|
|
13
26
|
continue;
|
|
@@ -29,13 +42,15 @@ export function collectFixPlan(result) {
|
|
|
29
42
|
// Fix: Create File from Template
|
|
30
43
|
if (error.artifactType === "file") {
|
|
31
44
|
if (error.template) {
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
45
|
+
// Normalize template path before resolving
|
|
46
|
+
const templateRel = toDoctrineRelativeTemplatePath(error.template);
|
|
47
|
+
// Verify template exists (resolving override)
|
|
48
|
+
const templatePath = resolveDoctrinePath(root, templateRel, activeFlowId);
|
|
49
|
+
if (templatePath && fs.existsSync(templatePath)) {
|
|
35
50
|
plan.actions.push({
|
|
36
51
|
kind: "createFileFromTemplate",
|
|
37
52
|
path: error.path,
|
|
38
|
-
template:
|
|
53
|
+
template: templateRel // Store NORMALIZED path
|
|
39
54
|
});
|
|
40
55
|
}
|
|
41
56
|
else {
|
|
@@ -71,6 +86,8 @@ export async function applyFixPlan(plan, options) {
|
|
|
71
86
|
paths: []
|
|
72
87
|
};
|
|
73
88
|
const root = repoRoot();
|
|
89
|
+
const state = loadState();
|
|
90
|
+
const activeFlowId = state.active.flow;
|
|
74
91
|
if (options.dryRun) {
|
|
75
92
|
console.log(kleur.bold().blue("Dry Run Fix Plan:"));
|
|
76
93
|
}
|
|
@@ -94,38 +111,27 @@ export async function applyFixPlan(plan, options) {
|
|
|
94
111
|
continue;
|
|
95
112
|
}
|
|
96
113
|
if (action.kind === "createFileFromTemplate" && action.template) {
|
|
97
|
-
|
|
114
|
+
// Re-resolve to get absolute path (base or flow)
|
|
115
|
+
// Template should already be normalized in collectFixPlan
|
|
116
|
+
const templatePath = resolveDoctrinePath(root, action.template, activeFlowId);
|
|
98
117
|
if (options.dryRun) {
|
|
99
118
|
console.log(` [COPY] ${action.path} (from ${action.template})`);
|
|
100
119
|
continue;
|
|
101
120
|
}
|
|
102
121
|
// Check existence logic (safe overwrite)
|
|
103
|
-
// MISSING_ARTIFACT error implies it didn't exist during validate.
|
|
104
|
-
// But checking again just in case (race condition or manual change).
|
|
105
122
|
if (fs.existsSync(fullPath)) {
|
|
106
|
-
// It exists now?
|
|
107
123
|
if (options.yes) {
|
|
108
|
-
// Overwrite? Spec says "Missing artifacts (safe fixes)".
|
|
109
|
-
// If it now exists, we probably shouldn't touch it unless it's empty?
|
|
110
|
-
// Validator said it was missing.
|
|
111
|
-
// If it exists now, we skip it to be safe, or prompt.
|
|
112
|
-
// Prompt says "File exists and differs. Overwrite?".
|
|
113
|
-
// Let's compare logs? No, simplistic check.
|
|
114
|
-
// Actually, if it exists, it wasn't missing. Maybe the validator ran, then user created it.
|
|
115
|
-
// Only overwrite if explicit confirmation.
|
|
116
|
-
// But usually applyFixPlan follows validate immediately.
|
|
117
124
|
console.log(kleur.yellow(`Skipping ${action.path}: File exists.`));
|
|
118
125
|
continue;
|
|
119
126
|
}
|
|
120
127
|
else {
|
|
121
|
-
// Interactive prompt?
|
|
122
|
-
// "Default behavior: non-interactive safe fixes where no overwrite is needed"
|
|
123
|
-
// "If a fix would require overwriting... Prompt"
|
|
124
|
-
// Since we only fix MISSING artifacts, we assume we don't overwrite.
|
|
125
|
-
// If it exists, we skip.
|
|
126
128
|
continue;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
if (!templatePath || !fs.existsSync(templatePath)) {
|
|
132
|
+
console.error(kleur.red(`Template not found: ${action.template}`));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
129
135
|
try {
|
|
130
136
|
// Read template
|
|
131
137
|
const content = fs.readFileSync(templatePath, "utf-8");
|