@geraldmaron/construct 1.0.16 → 1.0.18
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 +4 -1
- package/bin/construct +236 -21
- package/bin/construct-postinstall.mjs +17 -2
- package/commands/design/flow.md +2 -0
- package/commands/design/ui.md +2 -0
- package/lib/cli-commands.mjs +65 -13
- package/lib/embed/semantic.mjs +5 -3
- package/lib/embedded-contract/audit.mjs +52 -0
- package/lib/embedded-contract/capability.mjs +179 -0
- package/lib/embedded-contract/contract-version.mjs +39 -0
- package/lib/embedded-contract/envelope.mjs +70 -0
- package/lib/embedded-contract/index.mjs +71 -0
- package/lib/embedded-contract/ingest.mjs +77 -0
- package/lib/embedded-contract/model-resolve.mjs +186 -0
- package/lib/embedded-contract/redaction.mjs +91 -0
- package/lib/embedded-contract/role-facts.mjs +66 -0
- package/lib/embedded-contract/triage.mjs +134 -0
- package/lib/embedded-contract/workflow-defs.mjs +125 -0
- package/lib/embedded-contract/workflow-invoke.mjs +218 -0
- package/lib/hooks/config-protection.mjs +12 -5
- package/lib/init-unified.mjs +36 -26
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +21 -3
- package/lib/validators/skills.mjs +21 -0
- package/package.json +12 -7
- package/scripts/sync-specialists.mjs +12 -3
- package/skills/ai/agent-dev.md +2 -0
- package/skills/ai/llm-security.md +2 -0
- package/skills/ai/ml-ops.md +2 -0
- package/skills/ai/orchestration-workflow.md +2 -0
- package/skills/ai/prompt-and-eval.md +2 -0
- package/skills/ai/prompt-optimizer.md +2 -0
- package/skills/ai/rag-system.md +2 -0
- package/skills/ai/trace-triage.md +36 -0
- package/skills/architecture/api-design.md +2 -0
- package/skills/architecture/caching.md +2 -0
- package/skills/architecture/cloud-native.md +2 -0
- package/skills/architecture/message-queue.md +2 -0
- package/skills/architecture/security-arch.md +2 -0
- package/skills/compliance/ai-disclosure.md +2 -0
- package/skills/compliance/data-privacy.md +2 -0
- package/skills/compliance/license-audit.md +2 -0
- package/skills/compliance/regulatory-review.md +2 -0
- package/skills/development/cpp.md +2 -0
- package/skills/development/go.md +2 -0
- package/skills/development/java.md +2 -0
- package/skills/development/kotlin.md +2 -0
- package/skills/development/mobile-crossplatform.md +2 -0
- package/skills/development/python.md +2 -0
- package/skills/development/rust.md +2 -0
- package/skills/development/shell.md +2 -0
- package/skills/development/swift.md +2 -0
- package/skills/development/typescript.md +2 -0
- package/skills/devops/ci-cd.md +2 -0
- package/skills/devops/containerization.md +2 -0
- package/skills/devops/cost-optimization.md +2 -0
- package/skills/devops/data-engineering.md +2 -0
- package/skills/devops/database.md +2 -0
- package/skills/devops/dependency-management.md +2 -0
- package/skills/devops/devsecops.md +2 -0
- package/skills/devops/git-workflow.md +2 -0
- package/skills/devops/incident-response.md +2 -0
- package/skills/devops/monorepo.md +2 -0
- package/skills/devops/observability.md +2 -0
- package/skills/devops/performance.md +2 -0
- package/skills/devops/testing.md +2 -0
- package/skills/docs/adr-workflow.md +2 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -0
- package/skills/docs/customer-profile-workflow.md +2 -0
- package/skills/docs/document-ingest-workflow.md +2 -0
- package/skills/docs/evidence-ingest-workflow.md +2 -0
- package/skills/docs/init-docs.md +2 -0
- package/skills/docs/init-project.md +2 -0
- package/skills/docs/memo-and-decision-capture.md +45 -0
- package/skills/docs/prd-workflow.md +2 -0
- package/skills/docs/prfaq-workflow.md +2 -0
- package/skills/docs/product-intelligence-review.md +2 -0
- package/skills/docs/product-intelligence-workflow.md +2 -0
- package/skills/docs/product-signal-workflow.md +2 -0
- package/skills/docs/research-workflow.md +2 -0
- package/skills/docs/runbook-workflow.md +2 -0
- package/skills/docs/strategy-workflow.md +2 -0
- package/skills/docs/transcript-synthesis.md +43 -0
- package/skills/exploration/dependency-graph-reading.md +2 -0
- package/skills/exploration/repo-map.md +2 -0
- package/skills/exploration/tracer-bullet-method.md +2 -0
- package/skills/exploration/unknown-codebase-onboarding.md +2 -0
- package/skills/frameworks/django.md +2 -0
- package/skills/frameworks/nextjs.md +2 -0
- package/skills/frameworks/react.md +2 -0
- package/skills/frameworks/spring-boot.md +2 -0
- package/skills/frontend-design/accessibility.md +2 -0
- package/skills/frontend-design/component-patterns.md +2 -0
- package/skills/frontend-design/engineering.md +2 -0
- package/skills/frontend-design/screen-reader-testing.md +34 -0
- package/skills/frontend-design/state-management.md +2 -0
- package/skills/frontend-design/ui-aesthetics.md +2 -0
- package/skills/frontend-design/ux-principles.md +2 -0
- package/skills/operating/change-management.md +2 -0
- package/skills/operating/incident-response.md +2 -0
- package/skills/operating/oncall-rotation.md +2 -0
- package/skills/operating/orchestration-reference.md +2 -0
- package/skills/operating/raw-data-structuring.md +44 -0
- package/skills/operating/unstructured-triage.md +45 -0
- package/skills/quality-gates/premortem.md +37 -0
- package/skills/quality-gates/review-work.md +2 -0
- package/skills/quality-gates/verify-change.md +2 -0
- package/skills/quality-gates/verify-module.md +2 -0
- package/skills/quality-gates/verify-quality.md +2 -0
- package/skills/quality-gates/verify-security.md +2 -0
- package/skills/roles/architect.ai-systems.md +2 -0
- package/skills/roles/architect.data.md +2 -0
- package/skills/roles/architect.enterprise.md +2 -0
- package/skills/roles/architect.integration.md +2 -0
- package/skills/roles/architect.md +2 -0
- package/skills/roles/architect.platform.md +2 -0
- package/skills/roles/data-analyst.experiment.md +2 -0
- package/skills/roles/data-analyst.md +2 -0
- package/skills/roles/data-analyst.product-intelligence.md +2 -0
- package/skills/roles/data-analyst.product.md +2 -0
- package/skills/roles/data-analyst.telemetry.md +2 -0
- package/skills/roles/data-engineer.pipeline.md +2 -0
- package/skills/roles/data-engineer.vector-retrieval.md +2 -0
- package/skills/roles/data-engineer.warehouse.md +2 -0
- package/skills/roles/debugger.md +2 -0
- package/skills/roles/designer.accessibility.md +2 -0
- package/skills/roles/designer.md +2 -0
- package/skills/roles/engineer.ai.md +2 -0
- package/skills/roles/engineer.data.md +2 -0
- package/skills/roles/engineer.md +2 -0
- package/skills/roles/engineer.platform.md +2 -0
- package/skills/roles/operator.docs.md +2 -0
- package/skills/roles/operator.md +2 -0
- package/skills/roles/operator.release.md +2 -0
- package/skills/roles/operator.sre.md +2 -0
- package/skills/roles/orchestrator.md +2 -0
- package/skills/roles/product-manager.ai-product.md +2 -0
- package/skills/roles/product-manager.business-strategy.md +2 -0
- package/skills/roles/product-manager.enterprise.md +2 -0
- package/skills/roles/product-manager.growth.md +2 -0
- package/skills/roles/product-manager.md +2 -0
- package/skills/roles/product-manager.platform.md +2 -0
- package/skills/roles/product-manager.product.md +2 -0
- package/skills/roles/qa.ai-eval.md +2 -0
- package/skills/roles/qa.api-contract.md +2 -0
- package/skills/roles/qa.data-pipeline.md +2 -0
- package/skills/roles/qa.md +2 -0
- package/skills/roles/qa.test-automation.md +2 -0
- package/skills/roles/qa.web-ui.md +2 -0
- package/skills/roles/researcher.explorer.md +2 -0
- package/skills/roles/researcher.md +2 -0
- package/skills/roles/researcher.ux.md +2 -0
- package/skills/roles/reviewer.devil-advocate.md +2 -0
- package/skills/roles/reviewer.evaluator.md +2 -0
- package/skills/roles/reviewer.md +2 -0
- package/skills/roles/reviewer.trace.md +2 -0
- package/skills/roles/security.ai.md +2 -0
- package/skills/roles/security.appsec.md +2 -0
- package/skills/roles/security.cloud.md +2 -0
- package/skills/roles/security.legal-compliance.md +2 -0
- package/skills/roles/security.md +2 -0
- package/skills/roles/security.privacy.md +2 -0
- package/skills/roles/security.supply-chain.md +2 -0
- package/skills/security/blue-team.md +2 -0
- package/skills/security/code-audit.md +2 -0
- package/skills/security/pentest.md +2 -0
- package/skills/security/red-team.md +2 -0
- package/skills/security/threat-intel.md +2 -0
- package/skills/security/vuln-research.md +2 -0
- package/skills/strategy/competitive-landscape.md +2 -0
- package/skills/strategy/jobs-to-be-done.md +38 -0
- package/skills/strategy/market-research-methods.md +2 -0
- package/skills/strategy/narrative-arc.md +2 -0
- package/skills/strategy/pricing-positioning.md +2 -0
- package/skills/utility/clean-code.md +2 -0
- package/specialists/prompts/cx-architect.md +1 -0
- package/specialists/prompts/cx-designer.md +1 -1
- package/specialists/prompts/cx-engineer.md +1 -1
- package/specialists/prompts/cx-operations.md +1 -0
- package/specialists/prompts/cx-rd-lead.md +1 -0
- package/specialists/prompts/cx-sre.md +1 -0
- package/specialists/registry.json +20 -11
- package/specialists/role-manifests.json +1 -1
- package/templates/docs/test-plan.md +96 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/audit.mjs — approval gating and provenance identity for the ECL.
|
|
3
|
+
*
|
|
4
|
+
* Read-only contracts (capability, triage, model resolution) never write. The
|
|
5
|
+
* workflow-invocation contract is the only surface that can mutate durable
|
|
6
|
+
* state, and only when its approval mode permits. This module is the single
|
|
7
|
+
* place that maps an approval mode plus the deployment mode to a write gate, so
|
|
8
|
+
* the rule "no unapproved writes, mandatory audit on team/enterprise writes"
|
|
9
|
+
* lives in one tested function rather than scattered across call sites.
|
|
10
|
+
*
|
|
11
|
+
* The provenance sink for durable writes (the hash-chained audit trail) is wired
|
|
12
|
+
* by the workflow-invocation surface where writes actually occur and can be
|
|
13
|
+
* asserted against a real trail; this module owns the identity (traceId) and the
|
|
14
|
+
* gate decision.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
|
|
19
|
+
export const APPROVAL_MODES = ['proposal-only', 'requires-human-approval', 'allow-durable-write'];
|
|
20
|
+
export const DEFAULT_APPROVAL_MODE = 'proposal-only';
|
|
21
|
+
|
|
22
|
+
export function isValidApprovalMode(mode) {
|
|
23
|
+
return typeof mode === 'string' && APPROVAL_MODES.includes(mode);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Mint a trace identifier for one contract invocation. Carried in workflow
|
|
28
|
+
* responses so an embedder can correlate the call with downstream provenance.
|
|
29
|
+
*
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function newTraceId() {
|
|
33
|
+
return `ecl-${randomUUID()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve whether durable writes are permitted for an invocation, whether the
|
|
38
|
+
* caller must collect human approval first, and whether the write must be
|
|
39
|
+
* audited. Unknown modes fall back to the safest mode (proposal-only).
|
|
40
|
+
*
|
|
41
|
+
* @param {object} opts
|
|
42
|
+
* @param {string} [opts.approvalMode]
|
|
43
|
+
* @param {string} [opts.deploymentMode]
|
|
44
|
+
* @returns {{approvalMode:string,allowWrites:boolean,requiresApproval:boolean,mandatoryAudit:boolean}}
|
|
45
|
+
*/
|
|
46
|
+
export function resolveWriteGate({ approvalMode = DEFAULT_APPROVAL_MODE, deploymentMode = 'solo' } = {}) {
|
|
47
|
+
const mode = isValidApprovalMode(approvalMode) ? approvalMode : DEFAULT_APPROVAL_MODE;
|
|
48
|
+
const allowWrites = mode === 'allow-durable-write';
|
|
49
|
+
const requiresApproval = mode === 'requires-human-approval';
|
|
50
|
+
const mandatoryAudit = allowWrites && (deploymentMode === 'team' || deploymentMode === 'enterprise');
|
|
51
|
+
return { approvalMode: mode, allowWrites, requiresApproval, mandatoryAudit };
|
|
52
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/capability.mjs — capability discovery contract.
|
|
3
|
+
*
|
|
4
|
+
* Builds the read-only, secret-free description of what this Construct install
|
|
5
|
+
* can do: versions, the contract interfaces (CLI/MCP/SDK), roles, skills,
|
|
6
|
+
* workflows, schemas, models/providers, policies, telemetry posture, and
|
|
7
|
+
* plugins. Every section reads from the live registries so the published
|
|
8
|
+
* contract cannot drift from reality, and each section is independently
|
|
9
|
+
* guarded — a failing source degrades to a warning rather than breaking
|
|
10
|
+
* discovery. Provider data carries env-key NAMES and a configured boolean only,
|
|
11
|
+
* never a credential value; the envelope's no-secrets guard is the backstop.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, relative } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
import { getInstalledVersion } from '../version.mjs';
|
|
19
|
+
import { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION } from './contract-version.mjs';
|
|
20
|
+
import { listRoles } from '../roles/catalog.mjs';
|
|
21
|
+
import { listModelFamilies } from '../model-router.mjs';
|
|
22
|
+
import { resolveModelTiers } from '../model-registry.mjs';
|
|
23
|
+
import { readSkillFrontmatter } from '../sync/skill-frontmatter.mjs';
|
|
24
|
+
import { loadPluginRegistry } from '../plugin-registry.mjs';
|
|
25
|
+
import { listWorkflowDefs } from './workflow-defs.mjs';
|
|
26
|
+
import { APPROVAL_MODES } from './audit.mjs';
|
|
27
|
+
|
|
28
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
29
|
+
|
|
30
|
+
// The interfaces section describes the three transports every contract is
|
|
31
|
+
// exposed on; each carries the contract version so an embedder can negotiate.
|
|
32
|
+
|
|
33
|
+
function buildInterfaces() {
|
|
34
|
+
return [
|
|
35
|
+
{ surface: 'cli', contractVersion: CONTRACT_VERSION, entrypoints: ['construct capability describe --json', 'construct models resolve --json', 'construct intake classify --json', 'construct graph recommend --json', 'construct workflow invoke --json'] },
|
|
36
|
+
{ surface: 'mcp', contractVersion: CONTRACT_VERSION, tools: ['capability_describe', 'model_resolve', 'triage_recommend', 'workflow_invoke'] },
|
|
37
|
+
{ surface: 'sdk', contractVersion: CONTRACT_VERSION, module: 'construct/embedded-contract', exports: ['describeCapabilities', 'resolveEmbeddedModel', 'recommendPlan', 'invokeWorkflow'] },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listSkillIds(skillsDir, prefix = '') {
|
|
42
|
+
let out = [];
|
|
43
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
const skillMd = join(skillsDir, entry.name, 'SKILL.md');
|
|
46
|
+
if (existsSync(skillMd)) out.push(`${prefix}${entry.name}`);
|
|
47
|
+
else out = out.concat(listSkillIds(join(skillsDir, entry.name), `${prefix}${entry.name}/`));
|
|
48
|
+
} else if (entry.name.endsWith('.md') && entry.name !== 'SKILL.md' && entry.name !== 'routing.md') {
|
|
49
|
+
out.push(`${prefix}${entry.name.replace(/\.md$/, '')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildSkills(rootDir, warnings) {
|
|
56
|
+
const skillsDir = join(rootDir, 'skills');
|
|
57
|
+
if (!existsSync(skillsDir)) return [];
|
|
58
|
+
const ids = listSkillIds(skillsDir).sort();
|
|
59
|
+
let missingMetadata = 0;
|
|
60
|
+
const skills = ids.map((id) => {
|
|
61
|
+
const candidates = [join(skillsDir, `${id}.md`), join(skillsDir, id, 'SKILL.md')];
|
|
62
|
+
const file = candidates.find(existsSync);
|
|
63
|
+
const fm = file ? readSkillFrontmatter(readFileSync(file, 'utf8')) : null;
|
|
64
|
+
const inputs = Array.isArray(fm?.inputs) ? fm.inputs : null;
|
|
65
|
+
const artifactType = typeof fm?.artifactType === 'string' ? fm.artifactType : null;
|
|
66
|
+
if (!inputs || !artifactType) missingMetadata += 1;
|
|
67
|
+
return { id, description: fm?.description || null, inputs, artifactType };
|
|
68
|
+
});
|
|
69
|
+
if (missingMetadata > 0) {
|
|
70
|
+
warnings.push(`${missingMetadata}/${skills.length} skills have no structured inputs/artifactType yet (optional frontmatter; annotated incrementally).`);
|
|
71
|
+
}
|
|
72
|
+
return skills;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildSchemas(rootDir, warnings) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const dirs = [join(rootDir, 'lib', 'schemas'), join(rootDir, 'schemas')];
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
if (!existsSync(dir)) continue;
|
|
80
|
+
for (const file of readdirSync(dir)) {
|
|
81
|
+
if (!file.endsWith('.json')) continue;
|
|
82
|
+
const full = join(dir, file);
|
|
83
|
+
let version = null;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(full, 'utf8'));
|
|
86
|
+
version = parsed.version ?? parsed.$id ?? null;
|
|
87
|
+
} catch { warnings.push(`Schema ${file} could not be parsed.`); }
|
|
88
|
+
out.push({ id: file.replace(/\.json$/, ''), version, location: relative(rootDir, full) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildModels(env) {
|
|
95
|
+
const families = listModelFamilies({ env });
|
|
96
|
+
const providers = families.map((f) => ({
|
|
97
|
+
id: f.id,
|
|
98
|
+
label: f.label,
|
|
99
|
+
local: f.local,
|
|
100
|
+
requiresEnv: f.requiresEnv,
|
|
101
|
+
configured: f.configured,
|
|
102
|
+
healthStatus: 'unknown',
|
|
103
|
+
}));
|
|
104
|
+
const tiers = resolveModelTiers({ env });
|
|
105
|
+
return {
|
|
106
|
+
tiers: tiers.models,
|
|
107
|
+
tierSources: tiers.sources,
|
|
108
|
+
providers,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPolicies(rootDir, warnings) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = JSON.parse(readFileSync(join(rootDir, 'specialists', 'policy-inventory.json'), 'utf8'));
|
|
115
|
+
const arr = Array.isArray(raw) ? raw : (raw.policies || Object.values(raw));
|
|
116
|
+
return arr.map((p) => ({ id: p.id, enforcement: p.enforcement, mode: p.mode, description: p.description }));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
warnings.push(`Policy inventory unavailable: ${err.message}`);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Only public, secret-free plugin fields are surfaced — id, declared
|
|
124
|
+
// capabilities, built-in flag, and exposed MCP ids. Plugin manifests can carry
|
|
125
|
+
// requiredEnv and other config, so fields are picked explicitly rather than spread.
|
|
126
|
+
|
|
127
|
+
function buildPlugins(rootDir, env, warnings) {
|
|
128
|
+
try {
|
|
129
|
+
const reg = loadPluginRegistry({ cwd: rootDir, rootDir, env });
|
|
130
|
+
if (reg.errors?.length) warnings.push(`Plugin registry reported ${reg.errors.length} issue(s).`);
|
|
131
|
+
return (reg.plugins || []).map((p) => ({
|
|
132
|
+
id: p.id,
|
|
133
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities : [],
|
|
134
|
+
builtIn: Boolean(p.builtIn),
|
|
135
|
+
mcps: (p.mcps || []).map((m) => ({ id: m.id })),
|
|
136
|
+
}));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
warnings.push(`Plugin registry unavailable: ${err.message}`);
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildTelemetry(env) {
|
|
144
|
+
return {
|
|
145
|
+
tracingEnabled: Boolean(env.OTEL_EXPORTER_OTLP_ENDPOINT) && env.CONSTRUCT_OTEL !== 'off',
|
|
146
|
+
redaction: 'enabled',
|
|
147
|
+
note: 'Every contract response passes the no-secrets guard before serialization.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the capability contract. Pure read; returns a result object carrying a
|
|
153
|
+
* `warnings` array (lifted into the envelope by the calling surface).
|
|
154
|
+
*
|
|
155
|
+
* @param {object} [opts] { env, rootDir }
|
|
156
|
+
* @returns {object}
|
|
157
|
+
*/
|
|
158
|
+
export function buildCapabilityContract({ env = process.env, rootDir = REPO_ROOT } = {}) {
|
|
159
|
+
const warnings = [];
|
|
160
|
+
const { version: constructVersion, name } = getInstalledVersion();
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
product: name,
|
|
164
|
+
constructVersion,
|
|
165
|
+
contractVersion: CONTRACT_VERSION,
|
|
166
|
+
minClientContractVersion: MIN_CLIENT_CONTRACT_VERSION,
|
|
167
|
+
approvalModes: APPROVAL_MODES,
|
|
168
|
+
interfaces: buildInterfaces(),
|
|
169
|
+
roles: listRoles(),
|
|
170
|
+
skills: buildSkills(rootDir, warnings),
|
|
171
|
+
workflows: listWorkflowDefs(),
|
|
172
|
+
schemas: buildSchemas(rootDir, warnings),
|
|
173
|
+
models: buildModels(env),
|
|
174
|
+
policies: buildPolicies(rootDir, warnings),
|
|
175
|
+
telemetry: buildTelemetry(env),
|
|
176
|
+
plugins: buildPlugins(rootDir, env, warnings),
|
|
177
|
+
warnings,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/contract-version.mjs — version of the Embedded Contract Layer (ECL).
|
|
3
|
+
*
|
|
4
|
+
* The ECL is Construct's app-facing surface exposed over CLI-JSON, MCP, and
|
|
5
|
+
* SDK. Its schema evolves independently of the Construct
|
|
6
|
+
* package version, so it carries its own semver: additive changes bump the
|
|
7
|
+
* minor, breaking changes bump the major. Every contract envelope embeds
|
|
8
|
+
* CONTRACT_VERSION so an embedding application can negotiate compatibility
|
|
9
|
+
* without reading internal registries.
|
|
10
|
+
*
|
|
11
|
+
* Distinct from specialists/contracts.json `version` (handoff contracts) and
|
|
12
|
+
* lib/plugin-registry.mjs `MANIFEST_VERSION` (plugin manifests); those version
|
|
13
|
+
* unrelated internal surfaces.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parseSemver, compareSemver } from '../version.mjs';
|
|
17
|
+
|
|
18
|
+
export const CONTRACT_VERSION = '1.0.0';
|
|
19
|
+
|
|
20
|
+
// A client built against an older minor of the same major still works, because
|
|
21
|
+
// minor bumps are additive-only; a different major is incompatible either way.
|
|
22
|
+
|
|
23
|
+
export const MIN_CLIENT_CONTRACT_VERSION = '1.0.0';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a client built against `clientVersion` can talk to a server speaking
|
|
27
|
+
* `contractVersion`. Same major required; client minor must not exceed server.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} clientVersion
|
|
30
|
+
* @param {string} [contractVersion]
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isClientCompatible(clientVersion, contractVersion = CONTRACT_VERSION) {
|
|
34
|
+
const client = parseSemver(clientVersion);
|
|
35
|
+
const server = parseSemver(contractVersion);
|
|
36
|
+
if (!client || !server) return false;
|
|
37
|
+
if (client.major !== server.major) return false;
|
|
38
|
+
return compareSemver(clientVersion, contractVersion) <= 0;
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/envelope.mjs — common response envelope for every contract surface.
|
|
3
|
+
*
|
|
4
|
+
* CLI-JSON, MCP, and SDK all wrap their payload with `wrapResponse`, which
|
|
5
|
+
* stamps the contract version, the installed Construct version, the active
|
|
6
|
+
* deployment mode, and a generation timestamp, then runs the no-secrets guard
|
|
7
|
+
* before the payload can be serialized. Routing every surface through one
|
|
8
|
+
* envelope is what makes the three interfaces structurally identical and keeps
|
|
9
|
+
* the redaction guard impossible to bypass.
|
|
10
|
+
*
|
|
11
|
+
* `generatedAt` (and any per-call traceId carried in `data`) are volatile, so
|
|
12
|
+
* surface-parity tests compare envelopes with those fields excluded.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getInstalledVersion } from '../version.mjs';
|
|
16
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
17
|
+
import { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION } from './contract-version.mjs';
|
|
18
|
+
import { assertNoSecrets } from './redaction.mjs';
|
|
19
|
+
|
|
20
|
+
export const CONTRACT_SURFACES = ['cli', 'mcp', 'sdk'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a contract payload in the standard envelope and assert it carries no
|
|
24
|
+
* secrets.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {*} opts.data The contract-specific payload.
|
|
28
|
+
* @param {string} opts.surface One of CONTRACT_SURFACES.
|
|
29
|
+
* @param {string[]} [opts.warnings] Advisory messages for the caller.
|
|
30
|
+
* @param {Record<string,string>} [opts.env]
|
|
31
|
+
* @param {string} [opts.cwd]
|
|
32
|
+
* @param {string} [opts.generatedAt] ISO timestamp; injectable for deterministic tests.
|
|
33
|
+
* @returns {object}
|
|
34
|
+
*/
|
|
35
|
+
export function wrapResponse({ data, surface, warnings = [], env = process.env, cwd = process.cwd(), generatedAt } = {}) {
|
|
36
|
+
if (!CONTRACT_SURFACES.includes(surface)) {
|
|
37
|
+
throw new Error(`Unknown contract surface: ${surface}. Expected one of ${CONTRACT_SURFACES.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { version: constructVersion } = getInstalledVersion();
|
|
41
|
+
const deploymentMode = getDeploymentMode(env, { cwd });
|
|
42
|
+
|
|
43
|
+
const envelope = {
|
|
44
|
+
contractVersion: CONTRACT_VERSION,
|
|
45
|
+
minClientContractVersion: MIN_CLIENT_CONTRACT_VERSION,
|
|
46
|
+
constructVersion,
|
|
47
|
+
deploymentMode,
|
|
48
|
+
surface,
|
|
49
|
+
generatedAt: generatedAt || new Date().toISOString(),
|
|
50
|
+
warnings: Array.isArray(warnings) ? warnings : [],
|
|
51
|
+
data,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
assertNoSecrets(envelope, { env });
|
|
55
|
+
return envelope;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap a contract-core result for a surface. The core returns its payload with a
|
|
60
|
+
* `warnings` array as one field; this lifts `warnings` into the envelope and
|
|
61
|
+
* keeps the rest as `data`, so CLI, MCP, and SDK produce identical envelopes.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} result The contract core's return value (may carry `warnings`).
|
|
64
|
+
* @param {object} opts Forwarded to wrapResponse (surface, env, cwd, generatedAt).
|
|
65
|
+
* @returns {object}
|
|
66
|
+
*/
|
|
67
|
+
export function wrapContractResult(result, opts = {}) {
|
|
68
|
+
const { warnings = [], ...data } = result || {};
|
|
69
|
+
return wrapResponse({ ...opts, data, warnings });
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/index.mjs — SDK entrypoint for the Embedded Contract Layer.
|
|
3
|
+
*
|
|
4
|
+
* In-process applications import these functions to use Construct's app-facing
|
|
5
|
+
* contracts directly. Each returns the same versioned envelope the CLI-JSON and
|
|
6
|
+
* MCP surfaces emit, so a host can switch transports without reshaping its code.
|
|
7
|
+
* Published to package consumers via the package.json "exports" map.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { wrapContractResult } from './envelope.mjs';
|
|
11
|
+
import { resolveEmbeddedModel as resolveModelCore } from './model-resolve.mjs';
|
|
12
|
+
import { recommendPlan as recommendPlanCore } from './triage.mjs';
|
|
13
|
+
import { invokeWorkflow as invokeWorkflowCore } from './workflow-invoke.mjs';
|
|
14
|
+
import { buildCapabilityContract } from './capability.mjs';
|
|
15
|
+
|
|
16
|
+
export { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION, isClientCompatible } from './contract-version.mjs';
|
|
17
|
+
export { APPROVAL_MODES } from './audit.mjs';
|
|
18
|
+
|
|
19
|
+
// File→text resolution for SDK callers: extract a path through the real
|
|
20
|
+
// pipeline (docling/whisper/transcript), then pass { text, ingestion } to
|
|
21
|
+
// recommendPlan/invokeWorkflow. Keeps the plan/invoke cores synchronous.
|
|
22
|
+
|
|
23
|
+
export { resolveInput as extractFileForContract } from './ingest.mjs';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve which model an embedded workflow should use. Returns a versioned
|
|
27
|
+
* envelope; the resolution result is under `.data`.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} request See model-resolve.mjs resolveEmbeddedModel.
|
|
30
|
+
* @param {object} [opts] { env, cwd, registryPath }
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
export function resolveEmbeddedModel(request = {}, { env, cwd, registryPath } = {}) {
|
|
34
|
+
return wrapContractResult(resolveModelCore(request, { env, registryPath }), { surface: 'sdk', env, cwd });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Classify an artifact and return a role-aware plan without enqueuing or
|
|
39
|
+
* executing. Returns a versioned envelope; the plan is under `.data`.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} request See triage.mjs recommendPlan.
|
|
42
|
+
* @param {object} [opts] { env, cwd }
|
|
43
|
+
* @returns {object}
|
|
44
|
+
*/
|
|
45
|
+
export function recommendPlan(request = {}, { env, cwd } = {}) {
|
|
46
|
+
return wrapContractResult(recommendPlanCore(request, { env, cwd }), { surface: 'sdk', env, cwd });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Invoke an embedded workflow (roles/skills) with approval gating and
|
|
51
|
+
* provenance. Async; returns a versioned envelope with the execution plan under
|
|
52
|
+
* `.data`. Durable writes occur only when approvalMode is allow-durable-write.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} request See workflow-invoke.mjs invokeWorkflow.
|
|
55
|
+
* @param {object} [opts] { env, cwd }
|
|
56
|
+
* @returns {Promise<object>}
|
|
57
|
+
*/
|
|
58
|
+
export async function invokeWorkflow(request = {}, { env, cwd } = {}) {
|
|
59
|
+
return wrapContractResult(await invokeWorkflowCore(request, { env, cwd }), { surface: 'sdk', env, cwd });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Describe what this Construct install can do (read-only, secret-free). Returns
|
|
64
|
+
* a versioned envelope; the capability contract is under `.data`.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} [opts] { env, cwd, rootDir }
|
|
67
|
+
* @returns {object}
|
|
68
|
+
*/
|
|
69
|
+
export function describeCapabilities({ env, cwd, rootDir } = {}) {
|
|
70
|
+
return wrapContractResult(buildCapabilityContract({ env, rootDir }), { surface: 'sdk', env, cwd });
|
|
71
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/ingest.mjs — file→text resolution for embedded contracts.
|
|
3
|
+
*
|
|
4
|
+
* The embedded triage and workflow contracts accept either inline text or a file
|
|
5
|
+
* path. A file path is resolved through Construct's real extraction pipeline
|
|
6
|
+
* (`extractDocumentTextAsync`: docling for PDF/Office, whisper for audio/video,
|
|
7
|
+
* transcript/calendar/email extractors) rather than read as raw bytes, so an
|
|
8
|
+
* external application can hand Construct any supported artifact. Resolution is
|
|
9
|
+
* honest: a missing ASR backend or an unsupported type returns a structured
|
|
10
|
+
* error or a clearly-flagged best-effort fallback — never silent gibberish.
|
|
11
|
+
*
|
|
12
|
+
* Surfaces (CLI/MCP/SDK) own I/O and call this; the contract cores stay pure and
|
|
13
|
+
* receive the resolved text plus an `ingestion` metadata block to echo back.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { extname } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { extractDocumentTextAsync, isExtractableDocumentPath } from '../document-extract.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve contract input from inline text or a file path.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {string} [opts.input] Inline text (returned as-is when provided).
|
|
26
|
+
* @param {string} [opts.filePath] Path to extract when no inline text is given.
|
|
27
|
+
* @param {number} [opts.maxChars]
|
|
28
|
+
* @returns {Promise<{text:string, ingestion:(object|null), error:(object|null)}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveInput({ input = '', filePath = '', maxChars = null } = {}) {
|
|
31
|
+
if (input && input.trim()) {
|
|
32
|
+
return { text: input, ingestion: null, error: null };
|
|
33
|
+
}
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
return { text: '', ingestion: null, error: null };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isExtractableDocumentPath(filePath)) {
|
|
39
|
+
try {
|
|
40
|
+
const extracted = await extractDocumentTextAsync(filePath, { maxChars });
|
|
41
|
+
return {
|
|
42
|
+
text: extracted.text || '',
|
|
43
|
+
ingestion: {
|
|
44
|
+
sourcePath: filePath,
|
|
45
|
+
extractionMethod: extracted.extractionMethod,
|
|
46
|
+
characters: extracted.characters,
|
|
47
|
+
truncated: extracted.truncated,
|
|
48
|
+
droppedInfo: extracted.droppedInfo || [],
|
|
49
|
+
},
|
|
50
|
+
error: null,
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err && err.code === 'ASR_REQUIRED') {
|
|
54
|
+
return { text: '', ingestion: { sourcePath: filePath, extractionMethod: 'asr-required', droppedInfo: [] }, error: { code: 'ASR_REQUIRED', reason: err.message, remediation: 'Install whisper-cli (brew install whisper-cpp) or transcribe the file before sending.' } };
|
|
55
|
+
}
|
|
56
|
+
if (err && /WHISPER_BINARY_MISSING/.test(String(err.message))) {
|
|
57
|
+
return { text: '', ingestion: { sourcePath: filePath, extractionMethod: 'asr-required', droppedInfo: [] }, error: { code: 'ASR_REQUIRED', reason: err.message, remediation: 'Install whisper-cli (brew install whisper-cpp) to transcribe audio/video.' } };
|
|
58
|
+
}
|
|
59
|
+
return { text: '', ingestion: { sourcePath: filePath, extractionMethod: 'failed', droppedInfo: [] }, error: { code: 'EXTRACTION_FAILED', reason: err.message, remediation: 'Verify the file is readable and of a supported type.' } };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extensions outside the extractor registry (e.g. .parquet, an unknown
|
|
64
|
+
// suffix) fall back to a flagged raw read so plain-text data still classifies
|
|
65
|
+
// rather than failing outright. Recognized text types including .csv/.tsv are
|
|
66
|
+
// handled above via the extraction pipeline's UTF-8 path.
|
|
67
|
+
try {
|
|
68
|
+
const text = readFileSync(filePath, 'utf8');
|
|
69
|
+
return {
|
|
70
|
+
text,
|
|
71
|
+
ingestion: { sourcePath: filePath, extractionMethod: 'raw-utf8', droppedInfo: [], note: `No structured extractor for ${extname(filePath) || 'this type'}; read as plain text.` },
|
|
72
|
+
error: null,
|
|
73
|
+
};
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { text: '', ingestion: { sourcePath: filePath, extractionMethod: 'failed', droppedInfo: [] }, error: { code: 'FILE_UNREADABLE', reason: err.message, remediation: 'Check the path and permissions.' } };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/model-resolve.mjs — embedded model resolution contract.
|
|
3
|
+
*
|
|
4
|
+
* Given a host/IDE's provider context, resolve which model an embedded Construct
|
|
5
|
+
* workflow should use, following a fixed precedence:
|
|
6
|
+
* 1. host-model — the host's model, when recognized
|
|
7
|
+
* 2. same-family-fallback — a tier model in the host model's provider family
|
|
8
|
+
* 3. tier-default — Construct's configured tier default (env/config/registry)
|
|
9
|
+
* 4. config error — structured error with remediation when nothing resolves
|
|
10
|
+
*
|
|
11
|
+
* Cross-provider fallback is only taken when the caller opts in; otherwise host
|
|
12
|
+
* context that cannot be honored within its family yields a config error rather
|
|
13
|
+
* than silently switching providers. The contract never reads or returns a
|
|
14
|
+
* credential value — `requiresCredential` is a derived boolean — and never
|
|
15
|
+
* claims provider health it cannot verify (`healthStatus` defaults to unknown).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describeModelFamily, listModelFamilies } from '../model-router.mjs';
|
|
19
|
+
import { resolveModelTiers } from '../model-registry.mjs';
|
|
20
|
+
import { resolveProviderCapabilitiesSync } from '../provider-capabilities.js';
|
|
21
|
+
|
|
22
|
+
const VALID_TIERS = ['reasoning', 'standard', 'fast'];
|
|
23
|
+
const DEFAULT_TIER = 'standard';
|
|
24
|
+
|
|
25
|
+
// Workflow types lean toward deeper reasoning or faster turnaround; this maps a
|
|
26
|
+
// workflowType to a tier only when the caller did not request one explicitly.
|
|
27
|
+
|
|
28
|
+
const WORKFLOW_TIER_HINTS = {
|
|
29
|
+
'architecture-review': 'reasoning',
|
|
30
|
+
'risk-review': 'reasoning',
|
|
31
|
+
'research-synthesis': 'reasoning',
|
|
32
|
+
'prd-draft': 'standard',
|
|
33
|
+
'proposal-review': 'standard',
|
|
34
|
+
'evidence-ingest': 'fast',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function normalizeTier(tier) {
|
|
38
|
+
return VALID_TIERS.includes(tier) ? tier : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveTier({ requestedTier, workflowType }) {
|
|
42
|
+
return normalizeTier(requestedTier) || WORKFLOW_TIER_HINTS[workflowType] || DEFAULT_TIER;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Capability matching is best-effort against the model's known capability flags;
|
|
46
|
+
// names that cannot be confirmed are surfaced as warnings rather than asserted.
|
|
47
|
+
|
|
48
|
+
function matchCapabilities(modelId, requested, warnings) {
|
|
49
|
+
if (!Array.isArray(requested) || requested.length === 0) return [];
|
|
50
|
+
const caps = resolveProviderCapabilitiesSync(modelId);
|
|
51
|
+
const matched = [];
|
|
52
|
+
const unverified = [];
|
|
53
|
+
for (const name of requested) {
|
|
54
|
+
const key = String(name);
|
|
55
|
+
if (caps[key] === true) matched.push(key);
|
|
56
|
+
else unverified.push(key);
|
|
57
|
+
}
|
|
58
|
+
if (unverified.length) {
|
|
59
|
+
warnings.push(`Capability not confirmed for ${modelId}: ${unverified.join(', ')} (capability verification is best-effort).`);
|
|
60
|
+
}
|
|
61
|
+
return matched;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function familyByProvider(hostProvider, env) {
|
|
65
|
+
if (!hostProvider || typeof hostProvider !== 'string') return null;
|
|
66
|
+
const families = listModelFamilies({ env });
|
|
67
|
+
return families.find((f) => f.id === hostProvider)
|
|
68
|
+
|| families.find((f) => f.id === `openrouter-${hostProvider}`)
|
|
69
|
+
|| null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function success({ selectedModel, family, tier, resolutionSource, fallbackReason = null, tierSource = null, capabilities, warnings, env }) {
|
|
73
|
+
return {
|
|
74
|
+
selectedModel,
|
|
75
|
+
selectedProvider: family ? family.id : null,
|
|
76
|
+
providerFamily: family ? family.id : null,
|
|
77
|
+
resolutionSource,
|
|
78
|
+
requestedTier: tier,
|
|
79
|
+
fallbackReason,
|
|
80
|
+
tierSource,
|
|
81
|
+
capabilitiesMatched: matchCapabilities(selectedModel, capabilities, warnings),
|
|
82
|
+
healthStatus: 'unknown',
|
|
83
|
+
estimatedLimits: null,
|
|
84
|
+
requiresCredential: family ? !family.local && !family.configured : true,
|
|
85
|
+
error: null,
|
|
86
|
+
warnings,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function configError({ tier, reason, remediation, warnings }) {
|
|
91
|
+
return {
|
|
92
|
+
selectedModel: null,
|
|
93
|
+
selectedProvider: null,
|
|
94
|
+
providerFamily: null,
|
|
95
|
+
resolutionSource: 'config-error',
|
|
96
|
+
requestedTier: tier,
|
|
97
|
+
fallbackReason: null,
|
|
98
|
+
tierSource: null,
|
|
99
|
+
capabilitiesMatched: [],
|
|
100
|
+
healthStatus: 'unknown',
|
|
101
|
+
estimatedLimits: null,
|
|
102
|
+
requiresCredential: null,
|
|
103
|
+
error: { code: 'MODEL_UNRESOLVED', reason, remediation },
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the model an embedded workflow should use. Pure and synchronous;
|
|
110
|
+
* returns a result object carrying a `warnings` array (lifted into the envelope
|
|
111
|
+
* by the calling surface).
|
|
112
|
+
*
|
|
113
|
+
* @param {object} request
|
|
114
|
+
* @param {string} [request.workflowType]
|
|
115
|
+
* @param {string} [request.requestedTier] reasoning | standard | fast
|
|
116
|
+
* @param {string} [request.host]
|
|
117
|
+
* @param {string} [request.hostModel]
|
|
118
|
+
* @param {string} [request.hostProvider]
|
|
119
|
+
* @param {string[]} [request.capabilities]
|
|
120
|
+
* @param {boolean} [request.allowCrossProviderFallback=false]
|
|
121
|
+
* @param {object} [opts]
|
|
122
|
+
* @param {Record<string,string>} [opts.env]
|
|
123
|
+
* @param {string} [opts.registryPath]
|
|
124
|
+
* @returns {object}
|
|
125
|
+
*/
|
|
126
|
+
export function resolveEmbeddedModel(request = {}, { env = process.env, registryPath = null } = {}) {
|
|
127
|
+
const { workflowType, requestedTier, hostModel, hostProvider, capabilities, allowCrossProviderFallback = false } = request;
|
|
128
|
+
const tier = resolveTier({ requestedTier, workflowType });
|
|
129
|
+
const warnings = [];
|
|
130
|
+
|
|
131
|
+
const hostFamily = hostModel ? describeModelFamily(hostModel, { env }) : null;
|
|
132
|
+
|
|
133
|
+
if (hostModel && hostFamily) {
|
|
134
|
+
return success({ selectedModel: hostModel, family: hostFamily, tier, resolutionSource: 'host-model', capabilities, warnings, env });
|
|
135
|
+
}
|
|
136
|
+
if (hostModel && !hostFamily) {
|
|
137
|
+
warnings.push(`Host model "${hostModel}" is not a recognized provider family; attempting same-family or tier fallback.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const family = hostFamily || familyByProvider(hostProvider, env);
|
|
141
|
+
if (family) {
|
|
142
|
+
const model = family.tiers[tier];
|
|
143
|
+
if (model) {
|
|
144
|
+
const fallbackReason = hostModel
|
|
145
|
+
? `Exact host model unavailable; resolved the ${tier} model in the same provider family (${family.id}).`
|
|
146
|
+
: `Resolved the ${tier} model within the host provider family (${family.id}).`;
|
|
147
|
+
return success({ selectedModel: model, family, tier, resolutionSource: 'same-family-fallback', fallbackReason, capabilities, warnings, env });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hadHostContext = Boolean(hostModel || hostProvider);
|
|
152
|
+
if (hadHostContext && !allowCrossProviderFallback) {
|
|
153
|
+
return configError({
|
|
154
|
+
tier,
|
|
155
|
+
reason: 'Host model/provider could not be honored within its family and cross-provider fallback is disabled.',
|
|
156
|
+
remediation: 'Pass allowCrossProviderFallback=true, supply a recognized hostModel, or configure a same-family credential.',
|
|
157
|
+
warnings,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const tiers = resolveModelTiers({ env, registryPath });
|
|
162
|
+
const defaultModel = tiers.models[tier];
|
|
163
|
+
if (defaultModel) {
|
|
164
|
+
const fallbackReason = hadHostContext
|
|
165
|
+
? 'Host context could not be honored within its family; used the Construct tier default after cross-provider fallback was permitted.'
|
|
166
|
+
: 'No host context supplied; used the Construct tier default.';
|
|
167
|
+
return success({
|
|
168
|
+
selectedModel: defaultModel,
|
|
169
|
+
family: describeModelFamily(defaultModel, { env }),
|
|
170
|
+
tier,
|
|
171
|
+
resolutionSource: 'tier-default',
|
|
172
|
+
fallbackReason,
|
|
173
|
+
tierSource: tiers.sources[tier] || 'default',
|
|
174
|
+
capabilities,
|
|
175
|
+
warnings,
|
|
176
|
+
env,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return configError({
|
|
181
|
+
tier,
|
|
182
|
+
reason: `No model could be resolved for the ${tier} tier.`,
|
|
183
|
+
remediation: 'Set CX_MODEL_REASONING/STANDARD/FAST or configure a provider credential.',
|
|
184
|
+
warnings,
|
|
185
|
+
});
|
|
186
|
+
}
|