@geraldmaron/construct 1.0.15 → 1.0.17

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.
Files changed (198) hide show
  1. package/README.md +19 -3
  2. package/bin/construct +376 -98
  3. package/bin/construct-postinstall.mjs +17 -2
  4. package/db/schema/010_cx_scores.sql +51 -0
  5. package/lib/cli-commands.mjs +333 -147
  6. package/lib/contracts/validate.mjs +20 -10
  7. package/lib/contracts/violation-log.mjs +27 -12
  8. package/lib/embedded-contract/audit.mjs +52 -0
  9. package/lib/embedded-contract/capability.mjs +179 -0
  10. package/lib/embedded-contract/contract-version.mjs +39 -0
  11. package/lib/embedded-contract/envelope.mjs +70 -0
  12. package/lib/embedded-contract/index.mjs +71 -0
  13. package/lib/embedded-contract/ingest.mjs +77 -0
  14. package/lib/embedded-contract/model-resolve.mjs +186 -0
  15. package/lib/embedded-contract/redaction.mjs +91 -0
  16. package/lib/embedded-contract/role-facts.mjs +66 -0
  17. package/lib/embedded-contract/triage.mjs +134 -0
  18. package/lib/embedded-contract/workflow-defs.mjs +125 -0
  19. package/lib/embedded-contract/workflow-invoke.mjs +218 -0
  20. package/lib/hooks/config-protection.mjs +12 -5
  21. package/lib/hooks/stop-notify.mjs +7 -0
  22. package/lib/init-unified.mjs +36 -26
  23. package/lib/install/first-invocation.mjs +5 -1
  24. package/lib/intake/classify.mjs +6 -0
  25. package/lib/intake/prepare.mjs +22 -4
  26. package/lib/intake/tables/rnd.mjs +33 -0
  27. package/lib/integrations/intake-integrations.mjs +28 -2
  28. package/lib/mcp/server.mjs +69 -0
  29. package/lib/mcp/tools/embedded-contract.mjs +77 -0
  30. package/lib/mcp/tools/telemetry.mjs +30 -0
  31. package/lib/model-router.mjs +40 -0
  32. package/lib/op-log.mjs +61 -0
  33. package/lib/orchestration-policy.mjs +15 -0
  34. package/lib/roles/catalog.mjs +26 -95
  35. package/lib/roles/gateway.mjs +30 -1
  36. package/lib/scheduler/index.mjs +24 -4
  37. package/lib/server/insights.mjs +12 -0
  38. package/lib/service-manager.mjs +33 -11
  39. package/lib/setup.mjs +73 -10
  40. package/lib/update.mjs +31 -3
  41. package/lib/upgrade.mjs +31 -4
  42. package/lib/validators/skills.mjs +21 -0
  43. package/package.json +9 -3
  44. package/personas/construct.md +2 -0
  45. package/platforms/claude/CLAUDE.md +43 -15
  46. package/scripts/sync-specialists.mjs +32 -5
  47. package/skills/ai/agent-dev.md +2 -0
  48. package/skills/ai/llm-security.md +2 -0
  49. package/skills/ai/ml-ops.md +2 -0
  50. package/skills/ai/orchestration-workflow.md +2 -0
  51. package/skills/ai/prompt-and-eval.md +2 -0
  52. package/skills/ai/prompt-optimizer.md +2 -0
  53. package/skills/ai/rag-system.md +2 -0
  54. package/skills/ai/trace-triage.md +36 -0
  55. package/skills/architecture/api-design.md +2 -0
  56. package/skills/architecture/caching.md +2 -0
  57. package/skills/architecture/cloud-native.md +2 -0
  58. package/skills/architecture/message-queue.md +2 -0
  59. package/skills/architecture/security-arch.md +2 -0
  60. package/skills/compliance/ai-disclosure.md +2 -0
  61. package/skills/compliance/data-privacy.md +2 -0
  62. package/skills/compliance/license-audit.md +2 -0
  63. package/skills/compliance/regulatory-review.md +2 -0
  64. package/skills/development/cpp.md +2 -0
  65. package/skills/development/go.md +2 -0
  66. package/skills/development/java.md +2 -0
  67. package/skills/development/kotlin.md +2 -0
  68. package/skills/development/mobile-crossplatform.md +2 -0
  69. package/skills/development/python.md +2 -0
  70. package/skills/development/rust.md +2 -0
  71. package/skills/development/shell.md +2 -0
  72. package/skills/development/swift.md +2 -0
  73. package/skills/development/typescript.md +2 -0
  74. package/skills/devops/ci-cd.md +2 -0
  75. package/skills/devops/containerization.md +2 -0
  76. package/skills/devops/cost-optimization.md +2 -0
  77. package/skills/devops/data-engineering.md +2 -0
  78. package/skills/devops/database.md +2 -0
  79. package/skills/devops/dependency-management.md +2 -0
  80. package/skills/devops/devsecops.md +2 -0
  81. package/skills/devops/git-workflow.md +2 -0
  82. package/skills/devops/incident-response.md +2 -0
  83. package/skills/devops/monorepo.md +2 -0
  84. package/skills/devops/observability.md +2 -0
  85. package/skills/devops/performance.md +2 -0
  86. package/skills/devops/testing.md +2 -0
  87. package/skills/docs/adr-workflow.md +2 -0
  88. package/skills/docs/backlog-proposal-workflow.md +2 -0
  89. package/skills/docs/customer-profile-workflow.md +2 -0
  90. package/skills/docs/document-ingest-workflow.md +2 -0
  91. package/skills/docs/evidence-ingest-workflow.md +2 -0
  92. package/skills/docs/init-docs.md +2 -0
  93. package/skills/docs/init-project.md +2 -0
  94. package/skills/docs/memo-and-decision-capture.md +45 -0
  95. package/skills/docs/prd-workflow.md +2 -0
  96. package/skills/docs/prfaq-workflow.md +2 -0
  97. package/skills/docs/product-intelligence-review.md +2 -0
  98. package/skills/docs/product-intelligence-workflow.md +2 -0
  99. package/skills/docs/product-signal-workflow.md +2 -0
  100. package/skills/docs/research-workflow.md +2 -0
  101. package/skills/docs/runbook-workflow.md +2 -0
  102. package/skills/docs/strategy-workflow.md +2 -0
  103. package/skills/docs/transcript-synthesis.md +43 -0
  104. package/skills/exploration/dependency-graph-reading.md +2 -0
  105. package/skills/exploration/repo-map.md +2 -0
  106. package/skills/exploration/tracer-bullet-method.md +2 -0
  107. package/skills/exploration/unknown-codebase-onboarding.md +2 -0
  108. package/skills/frameworks/django.md +2 -0
  109. package/skills/frameworks/nextjs.md +2 -0
  110. package/skills/frameworks/react.md +2 -0
  111. package/skills/frameworks/spring-boot.md +2 -0
  112. package/skills/frontend-design/accessibility.md +2 -0
  113. package/skills/frontend-design/component-patterns.md +2 -0
  114. package/skills/frontend-design/engineering.md +2 -0
  115. package/skills/frontend-design/screen-reader-testing.md +34 -0
  116. package/skills/frontend-design/state-management.md +2 -0
  117. package/skills/frontend-design/ui-aesthetics.md +2 -0
  118. package/skills/frontend-design/ux-principles.md +2 -0
  119. package/skills/operating/change-management.md +2 -0
  120. package/skills/operating/incident-response.md +2 -0
  121. package/skills/operating/oncall-rotation.md +2 -0
  122. package/skills/operating/orchestration-reference.md +2 -0
  123. package/skills/operating/raw-data-structuring.md +44 -0
  124. package/skills/operating/unstructured-triage.md +45 -0
  125. package/skills/quality-gates/premortem.md +37 -0
  126. package/skills/quality-gates/review-work.md +2 -0
  127. package/skills/quality-gates/verify-change.md +2 -0
  128. package/skills/quality-gates/verify-module.md +2 -0
  129. package/skills/quality-gates/verify-quality.md +2 -0
  130. package/skills/quality-gates/verify-security.md +2 -0
  131. package/skills/roles/architect.ai-systems.md +2 -0
  132. package/skills/roles/architect.data.md +2 -0
  133. package/skills/roles/architect.enterprise.md +2 -0
  134. package/skills/roles/architect.integration.md +2 -0
  135. package/skills/roles/architect.md +2 -0
  136. package/skills/roles/architect.platform.md +2 -0
  137. package/skills/roles/data-analyst.experiment.md +2 -0
  138. package/skills/roles/data-analyst.md +2 -0
  139. package/skills/roles/data-analyst.product-intelligence.md +2 -0
  140. package/skills/roles/data-analyst.product.md +2 -0
  141. package/skills/roles/data-analyst.telemetry.md +2 -0
  142. package/skills/roles/data-engineer.pipeline.md +2 -0
  143. package/skills/roles/data-engineer.vector-retrieval.md +2 -0
  144. package/skills/roles/data-engineer.warehouse.md +2 -0
  145. package/skills/roles/debugger.md +2 -0
  146. package/skills/roles/designer.accessibility.md +2 -0
  147. package/skills/roles/designer.md +2 -0
  148. package/skills/roles/engineer.ai.md +2 -0
  149. package/skills/roles/engineer.data.md +2 -0
  150. package/skills/roles/engineer.md +2 -0
  151. package/skills/roles/engineer.platform.md +2 -0
  152. package/skills/roles/operator.docs.md +2 -0
  153. package/skills/roles/operator.md +2 -0
  154. package/skills/roles/operator.release.md +2 -0
  155. package/skills/roles/operator.sre.md +2 -0
  156. package/skills/roles/orchestrator.md +2 -0
  157. package/skills/roles/product-manager.ai-product.md +2 -0
  158. package/skills/roles/product-manager.business-strategy.md +2 -0
  159. package/skills/roles/product-manager.enterprise.md +2 -0
  160. package/skills/roles/product-manager.growth.md +2 -0
  161. package/skills/roles/product-manager.md +2 -0
  162. package/skills/roles/product-manager.platform.md +2 -0
  163. package/skills/roles/product-manager.product.md +2 -0
  164. package/skills/roles/qa.ai-eval.md +2 -0
  165. package/skills/roles/qa.api-contract.md +2 -0
  166. package/skills/roles/qa.data-pipeline.md +2 -0
  167. package/skills/roles/qa.md +2 -0
  168. package/skills/roles/qa.test-automation.md +2 -0
  169. package/skills/roles/qa.web-ui.md +2 -0
  170. package/skills/roles/researcher.explorer.md +2 -0
  171. package/skills/roles/researcher.md +2 -0
  172. package/skills/roles/researcher.ux.md +2 -0
  173. package/skills/roles/reviewer.devil-advocate.md +2 -0
  174. package/skills/roles/reviewer.evaluator.md +2 -0
  175. package/skills/roles/reviewer.md +2 -0
  176. package/skills/roles/reviewer.trace.md +2 -0
  177. package/skills/roles/security.ai.md +2 -0
  178. package/skills/roles/security.appsec.md +2 -0
  179. package/skills/roles/security.cloud.md +2 -0
  180. package/skills/roles/security.legal-compliance.md +2 -0
  181. package/skills/roles/security.md +2 -0
  182. package/skills/roles/security.privacy.md +2 -0
  183. package/skills/roles/security.supply-chain.md +2 -0
  184. package/skills/security/blue-team.md +2 -0
  185. package/skills/security/code-audit.md +2 -0
  186. package/skills/security/pentest.md +2 -0
  187. package/skills/security/red-team.md +2 -0
  188. package/skills/security/threat-intel.md +2 -0
  189. package/skills/security/vuln-research.md +2 -0
  190. package/skills/strategy/competitive-landscape.md +2 -0
  191. package/skills/strategy/jobs-to-be-done.md +38 -0
  192. package/skills/strategy/market-research-methods.md +2 -0
  193. package/skills/strategy/narrative-arc.md +2 -0
  194. package/skills/strategy/pricing-positioning.md +2 -0
  195. package/skills/utility/clean-code.md +2 -0
  196. package/specialists/prompts/cx-engineer.md +1 -1
  197. package/specialists/registry.json +18 -9
  198. package/specialists/role-manifests.json +1 -1
@@ -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
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * lib/embedded-contract/redaction.mjs — structural no-secrets guard for contract output.
3
+ *
4
+ * Every embedded-contract response passes through this module before
5
+ * serialization. `redact` masks values at secret-looking keys; `assertNoSecrets`
6
+ * is the load-bearing guard — it throws if any live credential value (drawn from
7
+ * the environment by key name) appears anywhere in the payload, so a contract
8
+ * surface can never leak a token even if a future field copies one in by
9
+ * mistake. The check is generic and decoupled from the provider registry: it
10
+ * derives the secret set from env keys at call time rather than a hardcoded list.
11
+ */
12
+
13
+ const SECRET_KEY_RE = /(api[_-]?key|secret|token|password|passwd|credential|authorization|bearer|cookie|session[_-]?id|private[_-]?key|client[_-]?secret|access[_-]?key)/i;
14
+
15
+ const REDACTED = '[redacted]';
16
+
17
+ // Credential env values shorter than this are treated as non-secret (empty
18
+ // flags, "1", port numbers) and excluded so the guard does not false-positive.
19
+
20
+ const MIN_SECRET_LENGTH = 8;
21
+
22
+ /**
23
+ * Deep-clone `value`, masking any value whose key name looks like a secret.
24
+ *
25
+ * @param {*} value
26
+ * @returns {*}
27
+ */
28
+ export function redact(value) {
29
+ if (Array.isArray(value)) return value.map((v) => redact(v));
30
+ if (value && typeof value === 'object') {
31
+ const out = {};
32
+ for (const [k, v] of Object.entries(value)) {
33
+ if (SECRET_KEY_RE.test(k) && (typeof v === 'string' || typeof v === 'number')) {
34
+ out[k] = REDACTED;
35
+ } else {
36
+ out[k] = redact(v);
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ return value;
42
+ }
43
+
44
+ /**
45
+ * Collect non-trivial environment values whose key name looks like a secret.
46
+ *
47
+ * @param {Record<string,string>} [env]
48
+ * @returns {Set<string>}
49
+ */
50
+ export function collectSecretValues(env = process.env) {
51
+ const out = new Set();
52
+ for (const [key, val] of Object.entries(env || {})) {
53
+ if (typeof val === 'string' && val.length >= MIN_SECRET_LENGTH && SECRET_KEY_RE.test(key)) {
54
+ out.add(val);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function walkStrings(value, visit, pathStr = '$') {
61
+ if (typeof value === 'string') {
62
+ visit(value, pathStr);
63
+ } else if (Array.isArray(value)) {
64
+ value.forEach((v, i) => walkStrings(v, visit, `${pathStr}[${i}]`));
65
+ } else if (value && typeof value === 'object') {
66
+ for (const [k, v] of Object.entries(value)) walkStrings(v, visit, `${pathStr}.${k}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Throw if any live credential value appears as a substring of any string in
72
+ * the payload. Returns the payload unchanged when clean.
73
+ *
74
+ * @param {*} value
75
+ * @param {{env?:Record<string,string>}} [opts]
76
+ * @returns {*}
77
+ */
78
+ export function assertNoSecrets(value, { env = process.env } = {}) {
79
+ const secrets = collectSecretValues(env);
80
+ if (secrets.size === 0) return value;
81
+ const leaks = [];
82
+ walkStrings(value, (str, pathStr) => {
83
+ for (const secret of secrets) {
84
+ if (str.includes(secret)) leaks.push(pathStr);
85
+ }
86
+ });
87
+ if (leaks.length) {
88
+ throw new Error(`Secret value leaked into contract output at: ${[...new Set(leaks)].join(', ')}`);
89
+ }
90
+ return value;
91
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * lib/embedded-contract/role-facts.mjs — shared role/contract enrichment.
3
+ *
4
+ * Both the triage/planning and workflow-invocation contracts need the same
5
+ * facts about a role chain: per-role rationale, the union of declared skills,
6
+ * and the evidence a role requires plus the outputs it must produce. Those facts
7
+ * come only from verifiable sources — the role catalog (registry) and the
8
+ * specialist contracts — so this module centralizes the lookups and keeps the
9
+ * two contracts consistent and free of invented requirements.
10
+ */
11
+
12
+ import { listRoles } from '../roles/catalog.mjs';
13
+ import { getIncomingContracts } from '../specialist-contracts.mjs';
14
+
15
+ /**
16
+ * Map of role id → catalog descriptor for the current registry.
17
+ * @returns {Map<string, object>}
18
+ */
19
+ export function roleMap() {
20
+ return new Map(listRoles().map((role) => [role.id, role]));
21
+ }
22
+
23
+ /**
24
+ * Per-role rationale for a chain, drawn from each role's catalog description.
25
+ *
26
+ * @param {string[]} chain
27
+ * @param {Map<string,object>} [map]
28
+ * @returns {Array<{role:string, reason:string}>}
29
+ */
30
+ export function roleRationale(chain, map = roleMap()) {
31
+ return chain.map((id) => ({
32
+ role: `cx-${id}`,
33
+ reason: map.get(id)?.description || 'No role description available.',
34
+ }));
35
+ }
36
+
37
+ /**
38
+ * Union of declared skills across a role chain, de-duplicated.
39
+ *
40
+ * @param {string[]} chain
41
+ * @param {Map<string,object>} [map]
42
+ * @returns {string[]}
43
+ */
44
+ export function skillsForChain(chain, map = roleMap()) {
45
+ return [...new Set(chain.flatMap((id) => map.get(id)?.skills || []))];
46
+ }
47
+
48
+ /**
49
+ * Evidence a role requires and the outputs it must produce, taken from the
50
+ * role's incoming specialist contract (input.mustContain + preconditions /
51
+ * output.mustContain). Returns empty arrays when no contract is declared.
52
+ *
53
+ * @param {string} roleId Bare role id (no cx- prefix).
54
+ * @returns {{evidenceRequirements:string[], expectedOutputs:string[]}}
55
+ */
56
+ export function contractFacts(roleId) {
57
+ const contracts = getIncomingContracts(`cx-${roleId}`) || [];
58
+ const evidence = new Set();
59
+ const outputs = new Set();
60
+ for (const c of contracts) {
61
+ for (const item of c.input?.mustContain || []) evidence.add(item);
62
+ for (const pre of c.preconditions || []) evidence.add(pre);
63
+ for (const item of c.output?.mustContain || []) outputs.add(item);
64
+ }
65
+ return { evidenceRequirements: [...evidence], expectedOutputs: [...outputs] };
66
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * lib/embedded-contract/triage.mjs — embedded triage and planning contract.
3
+ *
4
+ * Classifies an artifact and returns a role-aware plan without enqueuing or
5
+ * executing anything. Wraps the deterministic classifier (lib/intake/classify)
6
+ * and enriches it from real sources only: the role catalog supplies role
7
+ * rationale and skills, the specialist contracts supply evidence requirements
8
+ * and expected outputs. Classification confidence (how sure the classifier is)
9
+ * is reported as a distinct, labeled field from any downstream generation
10
+ * confidence, which is produced only when a workflow actually runs.
11
+ *
12
+ * Pure and side-effect-free: no queue write, no disk artifact, no model call.
13
+ */
14
+
15
+ import { classifyRdIntake } from '../intake/classify.mjs';
16
+ import { getDeploymentMode } from '../deployment-mode.mjs';
17
+ import { roleMap, roleRationale, skillsForChain, contractFacts } from './role-facts.mjs';
18
+ import { workflowTypeForIntake } from './workflow-defs.mjs';
19
+
20
+ const CONFIDENCE_FLOOR = 0.6;
21
+
22
+ function buildRiskFactors(triage) {
23
+ const factors = [];
24
+ if (triage.intakeType === 'unknown') factors.push('classification is unknown — no keywords matched');
25
+ if (typeof triage.confidence === 'number' && triage.confidence < CONFIDENCE_FLOOR) {
26
+ factors.push(`low classification confidence (${triage.confidence.toFixed(2)})`);
27
+ }
28
+ const ambiguous = (triage.rationale || '').includes('ambiguous');
29
+ if (ambiguous) factors.push('top two classifications are close (ambiguous)');
30
+ return factors;
31
+ }
32
+
33
+ /**
34
+ * Produce a role-aware plan for an artifact without enqueuing or executing it.
35
+ *
36
+ * @param {object} request
37
+ * @param {string} [request.input] Artifact text to classify.
38
+ * @param {string} [request.sourcePath] Filename/source hint for the classifier.
39
+ * @param {string} [request.artifactType] Optional artifact-type hint (advisory).
40
+ * @param {string} [request.domain]
41
+ * @param {string} [request.desiredOutcome]
42
+ * @param {string[]} [request.constraints]
43
+ * @param {string[]} [request.availableRoles] Restrict the plan to these role ids.
44
+ * @param {object} [request.profile]
45
+ * @param {object} [opts] { env, cwd }
46
+ * @returns {object}
47
+ */
48
+ export function recommendPlan(request = {}, { env = process.env, cwd = process.cwd() } = {}) {
49
+ const { input = '', sourcePath = '', artifactType, availableRoles, profile = null, ingestion = null } = request;
50
+ const warnings = [];
51
+
52
+ // Surface extraction provenance and flag low-yield/truncated inputs so a
53
+ // sparse classification is attributable to thin source text, not silent loss.
54
+
55
+ if (ingestion) {
56
+ if (ingestion.error) warnings.push(`ingestion: ${ingestion.error.code} — ${ingestion.error.reason}`);
57
+ for (const drop of ingestion.droppedInfo || []) {
58
+ warnings.push(`ingestion: ${drop.kind || 'dropped-content'}${drop.recoverable ? ' (recoverable)' : ''}.`);
59
+ }
60
+ if (ingestion.truncated) warnings.push('ingestion: source text was truncated before classification.');
61
+ if (ingestion.note) warnings.push(`ingestion: ${ingestion.note}`);
62
+ }
63
+
64
+ const triage = classifyRdIntake({ sourcePath: sourcePath || (artifactType ? `${artifactType}.md` : ''), extractedText: input, profile });
65
+ const roles = roleMap();
66
+
67
+ let chain = [...(triage.recommendedChain || [])];
68
+ const primaryOwner = triage.primaryOwner;
69
+
70
+ if (Array.isArray(availableRoles) && availableRoles.length) {
71
+ const allowed = new Set(availableRoles);
72
+ const dropped = chain.filter((r) => !allowed.has(r));
73
+ if (dropped.length) warnings.push(`Roles not in availableRoles were dropped from the chain: ${dropped.join(', ')}.`);
74
+ chain = chain.filter((r) => allowed.has(r));
75
+ if (primaryOwner && !allowed.has(primaryOwner)) {
76
+ warnings.push(`Primary owner ${primaryOwner} is not in availableRoles; the plan cannot be executed as recommended.`);
77
+ }
78
+ }
79
+
80
+ const rationale = roleRationale(chain, roles);
81
+ const suggestedSkills = skillsForChain(chain, roles);
82
+
83
+ const { evidenceRequirements, expectedOutputs } = contractFacts(primaryOwner);
84
+ if (!evidenceRequirements.length) {
85
+ warnings.push(`No declared evidence contract for cx-${primaryOwner}; evidence requirements are unspecified.`);
86
+ }
87
+
88
+ const deploymentMode = getDeploymentMode(env, { cwd });
89
+ const requiresApproval = Boolean(triage.requiresApproval) || deploymentMode === 'enterprise';
90
+ const approvalRequirements = {
91
+ requiresApproval,
92
+ reason: triage.requiresApproval
93
+ ? 'The classified work type requires approval before durable changes.'
94
+ : (deploymentMode === 'enterprise' ? 'Enterprise deployment mode mandates approval for durable changes.' : 'No approval required before proposing changes.'),
95
+ };
96
+
97
+ const ownerAvailable = !primaryOwner || !Array.isArray(availableRoles) || !availableRoles.length || availableRoles.includes(primaryOwner);
98
+ const canExecute = triage.intakeType !== 'unknown'
99
+ && typeof triage.confidence === 'number' && triage.confidence >= CONFIDENCE_FLOOR
100
+ && chain.length > 0
101
+ && ownerAvailable;
102
+ const canExecuteReason = canExecute
103
+ ? 'Classification is confident and maps to a role chain that can be invoked.'
104
+ : 'Execution is not recommended: classification is unknown, low-confidence, or no usable role chain remains.';
105
+
106
+ const nextStepOptions = [
107
+ { action: 'enqueue', description: 'Add to the intake queue for processing (construct intake process).' },
108
+ ];
109
+ if (canExecute) nextStepOptions.push({ action: 'invoke-workflow', description: 'Invoke the recommended role chain as an embedded workflow.' });
110
+ if (!canExecute) nextStepOptions.push({ action: 'clarify', description: 'Request more detail; classification confidence is low or unknown.' });
111
+
112
+ return {
113
+ classification: { intakeType: triage.intakeType, rdStage: triage.rdStage },
114
+ ingestion,
115
+ confidenceKind: 'classification',
116
+ classificationConfidence: triage.confidence,
117
+ primaryOwner,
118
+ recommendedAction: triage.recommendedAction,
119
+ recommendedChain: chain,
120
+ suggestedWorkflowType: workflowTypeForIntake(triage.intakeType),
121
+ roleRationale: rationale,
122
+ suggestedSkills,
123
+ evidenceRequirements,
124
+ expectedOutputs,
125
+ approvalRequirements,
126
+ risks: { level: triage.risk || 'unknown', factors: buildRiskFactors(triage) },
127
+ nextStepOptions,
128
+ canExecute,
129
+ canExecuteReason,
130
+ rationale: triage.rationale,
131
+ candidates: triage.candidates || [],
132
+ warnings,
133
+ };
134
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * lib/embedded-contract/workflow-defs.mjs — embedded workflow type definitions.
3
+ *
4
+ * The single source of truth for the workflow types an embedding application can
5
+ * invoke. Each definition names a default role chain (real registry role ids), a
6
+ * model tier, a default approval mode, an optional output-schema artifact, and a
7
+ * one-line description. Skills are not hardcoded here — they are derived from the
8
+ * selected roles' declared skills (role-facts) so this file never names a skill
9
+ * id that does not exist. Capability discovery reads these definitions so the
10
+ * published workflow list cannot drift from what invocation actually supports.
11
+ */
12
+
13
+ const DEFS = {
14
+ 'evidence-ingest': {
15
+ tier: 'fast',
16
+ defaultApprovalMode: 'proposal-only',
17
+ chain: ['researcher', 'data-analyst'],
18
+ outputSchema: null,
19
+ description: 'Ingest and structure raw evidence (notes, documents, signals) into a normalized summary.',
20
+ },
21
+ 'proposal-review': {
22
+ tier: 'standard',
23
+ defaultApprovalMode: 'requires-human-approval',
24
+ chain: ['reviewer', 'devil-advocate'],
25
+ outputSchema: 'review-report',
26
+ description: 'Review a proposal for correctness, risk, and hidden assumptions before acceptance.',
27
+ },
28
+ 'prd-draft': {
29
+ tier: 'standard',
30
+ defaultApprovalMode: 'proposal-only',
31
+ chain: ['product-manager', 'architect'],
32
+ outputSchema: 'decision',
33
+ description: 'Draft a product requirements document from a problem statement and supporting evidence.',
34
+ },
35
+ 'architecture-review': {
36
+ tier: 'reasoning',
37
+ defaultApprovalMode: 'requires-human-approval',
38
+ chain: ['architect', 'security', 'devil-advocate'],
39
+ outputSchema: 'review-report',
40
+ description: 'Review an architecture or design for trade-offs, failure modes, and security exposure.',
41
+ },
42
+ 'risk-review': {
43
+ tier: 'reasoning',
44
+ defaultApprovalMode: 'requires-human-approval',
45
+ chain: ['devil-advocate', 'security', 'legal-compliance'],
46
+ outputSchema: 'review-report',
47
+ description: 'Stress-test a plan for risk: failure modes, security, and compliance exposure.',
48
+ },
49
+ 'research-synthesis': {
50
+ tier: 'reasoning',
51
+ defaultApprovalMode: 'proposal-only',
52
+ chain: ['researcher', 'data-analyst', 'evaluator'],
53
+ outputSchema: null,
54
+ description: 'Synthesize multiple sources into a cited, evidence-graded research summary.',
55
+ },
56
+ 'transcript-process': {
57
+ tier: 'fast',
58
+ defaultApprovalMode: 'proposal-only',
59
+ chain: ['researcher', 'data-analyst'],
60
+ outputSchema: null,
61
+ description: 'Process a meeting/call transcript into a summary, decisions, and action items.',
62
+ },
63
+ 'data-structure': {
64
+ tier: 'standard',
65
+ defaultApprovalMode: 'proposal-only',
66
+ chain: ['data-analyst', 'data-engineer'],
67
+ outputSchema: null,
68
+ description: 'Parse, validate, and profile a raw dataset into a structured, described shape.',
69
+ },
70
+ 'memo-draft': {
71
+ tier: 'fast',
72
+ defaultApprovalMode: 'proposal-only',
73
+ chain: ['docs-keeper', 'reviewer'],
74
+ outputSchema: null,
75
+ description: 'Draft a decision or status memo from a problem statement and context.',
76
+ },
77
+ 'structure-notes': {
78
+ tier: 'fast',
79
+ defaultApprovalMode: 'proposal-only',
80
+ chain: ['orchestrator', 'researcher'],
81
+ outputSchema: null,
82
+ description: 'Structure an unclassified brain-dump or rough notes into a normalized summary with extracted intents.',
83
+ },
84
+ };
85
+
86
+ // Maps a classifier intakeType to the workflow type that would carry it out, so
87
+ // the triage contract can suggest a directly-invokable workflow. Returns null
88
+ // when no workflow covers the classification (the plan is not directly invokable).
89
+
90
+ const INTAKE_TO_WORKFLOW = {
91
+ proposal: 'proposal-review',
92
+ prd: 'prd-draft',
93
+ 'meta-prd': 'prd-draft',
94
+ architecture: 'architecture-review',
95
+ rfc: 'architecture-review',
96
+ risk: 'risk-review',
97
+ security: 'risk-review',
98
+ research: 'research-synthesis',
99
+ 'research-note': 'research-synthesis',
100
+ signal: 'evidence-ingest',
101
+ 'user-signal': 'evidence-ingest',
102
+ evidence: 'evidence-ingest',
103
+ memo: 'memo-draft',
104
+ transcript: 'transcript-process',
105
+ 'raw-data': 'data-structure',
106
+ unknown: 'structure-notes',
107
+ };
108
+
109
+ export const WORKFLOW_TYPES = Object.keys(DEFS);
110
+
111
+ export function getWorkflowDef(type) {
112
+ return DEFS[type] || null;
113
+ }
114
+
115
+ /**
116
+ * Public, secret-free description of every workflow type for capability discovery.
117
+ * @returns {Array<object>}
118
+ */
119
+ export function listWorkflowDefs() {
120
+ return WORKFLOW_TYPES.map((type) => ({ type, ...DEFS[type] }));
121
+ }
122
+
123
+ export function workflowTypeForIntake(intakeType) {
124
+ return INTAKE_TO_WORKFLOW[intakeType] || null;
125
+ }