@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
@@ -157,16 +157,24 @@ export function findContract({ producer, consumer, id, contractsPath = CONTRACTS
157
157
  * itself a contract violation (MCP best-practice 2025 — no silent skip on
158
158
  * tool-argument omission).
159
159
  */
160
- export function validateHandoff({
161
- producer,
162
- consumer,
163
- id,
164
- artifact,
165
- packet,
166
- contractsPath = CONTRACTS_PATH,
167
- repoRoot = REPO_ROOT,
168
- enforcement = 'block',
169
- }) {
160
+ export function validateHandoff(opts = {}) {
161
+ const {
162
+ producer,
163
+ consumer,
164
+ id,
165
+ artifact,
166
+ packet,
167
+ contractsPath = CONTRACTS_PATH,
168
+ repoRoot = REPO_ROOT,
169
+ enforcement = 'block',
170
+ } = opts;
171
+ // Distinguish caller-supplied repoRoot from the default. Only an explicit
172
+ // repoRoot routes the violation log write — the default REPO_ROOT is for
173
+ // joining schema paths in this module, and forwarding it to the logger
174
+ // would override the cwd-based project-scope resolver that the test
175
+ // suite (and runtime project state) relies on.
176
+ const explicitRepoRoot = Object.prototype.hasOwnProperty.call(opts, 'repoRoot') ? repoRoot : undefined;
177
+
170
178
  const errors = [];
171
179
  const postconditionFailures = [];
172
180
 
@@ -193,6 +201,7 @@ export function validateHandoff({
193
201
  logViolation(`${producer}->${consumer}`, 'output', errors, packet ?? artifact, {
194
202
  verdict: postconditionFailures.length > 0 ? 'BLOCKED_CONTRACT' : 'CONTRACT_VIOLATION',
195
203
  postconditionFailures,
204
+ repoRoot: explicitRepoRoot,
196
205
  });
197
206
  return enforcement === 'block'
198
207
  ? { ok: false, status: 'BLOCKED_CONTRACT', errors: [`no contract found for ${producer}→${consumer}${id ? ` id=${id}` : ''}`, ...errors], contract: null }
@@ -247,6 +256,7 @@ export function validateHandoff({
247
256
  logViolation(contract.id, 'output', errors, packet ?? artifact, {
248
257
  verdict,
249
258
  postconditionFailures,
259
+ repoRoot: explicitRepoRoot,
250
260
  });
251
261
 
252
262
  if (enforcement === 'block') return { ok: false, status: 'BLOCKED_CONTRACT', errors, contract };
@@ -39,10 +39,22 @@ const LAST_AGENT = join(CX_DIR, 'last-agent.json');
39
39
  // cwd/HOME changes inside the same process (tests, harness reuse) route
40
40
  // correctly.
41
41
 
42
- function logFile() {
42
+ function logFile(repoRoot) {
43
+ // Explicit repoRoot bypasses the marker-walk in resolveProjectScope: the
44
+ // caller has already decided the scope (test fixture, sandbox, embedded
45
+ // worktree). Without this, a fixture without a `.cx/` marker falls back
46
+ // to ~/.cx/ and pollutes the developer's home log.
47
+ if (repoRoot) return join(repoRoot, '.cx', 'contract-violations.jsonl');
43
48
  return resolveProjectScopedPath('contract-violations.jsonl', { ensureDir: false });
44
49
  }
45
50
 
51
+ // Exposed for diagnostic surfaces (e.g. doctor) that need to print the
52
+ // real path. Resolves on every call so cwd/HOME changes inside the same
53
+ // process route correctly. `repoRoot` overrides cwd-based resolution so
54
+ // callers running against a fixture (tests, sandboxes) can isolate their
55
+ // log writes from the developer's project log.
56
+ export function violationLogPath(repoRoot) { return logFile(repoRoot); }
57
+
46
58
  function sha256(input) { return createHash('sha256').update(input).digest('hex'); }
47
59
 
48
60
  function readLastAgent() {
@@ -50,44 +62,47 @@ function readLastAgent() {
50
62
  catch { return 'construct'; }
51
63
  }
52
64
 
53
- function readTailRecord() {
54
- const file = logFile();
65
+ function readTailRecord(repoRoot) {
66
+ const file = logFile(repoRoot);
55
67
  const lastLine = readLastLineAcrossSegments(file);
56
68
  if (!lastLine) return null;
57
69
  try { return JSON.parse(lastLine); }
58
70
  catch { return null; }
59
71
  }
60
72
 
61
- function readPrevLineHash() {
62
- const file = logFile();
73
+ function readPrevLineHash(repoRoot) {
74
+ const file = logFile(repoRoot);
63
75
  const lastLine = readLastLineAcrossSegments(file);
64
76
  return lastLine ? sha256(lastLine) : null;
65
77
  }
66
78
 
67
- function nextSequence() {
68
- const tail = readTailRecord();
79
+ function nextSequence(repoRoot) {
80
+ const tail = readTailRecord(repoRoot);
69
81
  const prior = Number.isInteger(tail?.sequence) ? tail.sequence : 0;
70
82
  return prior + 1;
71
83
  }
72
84
 
73
85
  /**
74
86
  * Append a violation record. Best-effort: file I/O failures are swallowed
75
- * so logging never crashes the caller.
87
+ * so logging never crashes the caller. `extra.repoRoot` (extracted, not
88
+ * persisted) routes the write to a fixture log when callers run against
89
+ * a tmpdir — required for test isolation.
76
90
  */
77
91
  export function logViolation(contractId, direction, missing, packet, extra = {}) {
78
92
  try {
79
- const file = logFile();
93
+ const { repoRoot, ...persistedExtra } = extra || {};
94
+ const file = logFile(repoRoot);
80
95
  mkdirSync(dirname(file), { recursive: true });
81
96
  const record = {
82
97
  ts: new Date().toISOString(),
83
- sequence: nextSequence(),
98
+ sequence: nextSequence(repoRoot),
84
99
  agent: readLastAgent(),
85
100
  contractId,
86
101
  direction,
87
102
  missing,
88
103
  packet_keys: packet && typeof packet === 'object' ? Object.keys(packet) : null,
89
- prev_line_hash: readPrevLineHash(),
90
- ...extra,
104
+ prev_line_hash: readPrevLineHash(repoRoot),
105
+ ...persistedExtra,
91
106
  };
92
107
  appendBounded('contract-violations', file, JSON.stringify(record) + '\n');
93
108
  } catch { /* logging is best-effort */ }
@@ -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
+ }