@geraldmaron/construct 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -148,7 +148,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
148
148
 
149
149
  | Command | What it does |
150
150
  |---|---|
151
- | `construct artifact` | Validate typed document artifacts against the release gate |
151
+ | `construct artifact` | Plan or locally execute manifest-backed artifact workflows with execution provenance |
152
152
  | `construct ask` | One-shot ask against the active knowledge index |
153
153
  | `construct bootstrap` | Import seed observation corpus into local memory store for cold-start acceleration |
154
154
  | `construct customer` | Manage customer profiles for product intelligence |
@@ -257,6 +257,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
257
257
  | `construct roles:set` | Activate a role contract |
258
258
  | `construct scheduler` | Manage scheduled background jobs (tag-mining, doc-hygiene, skill-rollup) |
259
259
  | `construct skills` | Skill relevance detection |
260
+ | `construct sources` | Manage typed integration source targets in construct.config.json |
260
261
  | `construct uninstall` | Remove Construct state |
261
262
  | `construct update` | Reinstall this checkout |
262
263
  | `construct upgrade` | Upgrade to latest npm version |
package/bin/construct CHANGED
@@ -500,15 +500,19 @@ async function cmdDoctor() {
500
500
 
501
501
  const checks = [];
502
502
  const add = (label, pass, optional = false) => checks.push({ label, pass, optional });
503
+ const { isConstructSourceCheckout } = await import('../lib/doctor/source-checkout.mjs');
504
+ const isSourceCheckout = isConstructSourceCheckout(ROOT_DIR);
503
505
  add('registry.json exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'registry.json')));
504
506
  add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
505
507
 
506
- // Dashboard static build output. The Next.js app under apps/dashboard
507
- // emits a static export into lib/server/static/ via its postbuild script;
508
- // an empty static dir means lib/server/index.mjs will serve 404s.
508
+ // Dashboard static build output is absent from published npm installs.
509
509
 
510
- const dashboardIndex = path.join(ROOT_DIR, 'lib', 'server', 'static', 'index.html');
511
- add('Dashboard built (lib/server/static/index.html)', fs.existsSync(dashboardIndex));
510
+ if (isSourceCheckout) {
511
+ const dashboardIndex = path.join(ROOT_DIR, 'lib', 'server', 'static', 'index.html');
512
+ add('Dashboard built (lib/server/static/index.html)', fs.existsSync(dashboardIndex));
513
+ } else {
514
+ add('Dashboard built (skipped — published package)', true, true);
515
+ }
512
516
 
513
517
  // Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
514
518
  // Only checks when cwd looks like a Construct project (has .cx/ or
@@ -858,8 +862,21 @@ async function cmdDoctor() {
858
862
  add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
859
863
  const skillAudit = auditSkills({ rootDir: ROOT_DIR, silent: true });
860
864
  add('No declared skills missing on disk', skillAudit.pass, false);
861
- const specialistAudit = auditSpecialists({ rootDir: ROOT_DIR, silent: true });
862
- add('Specialist/skill audit cross-checks', specialistAudit.pass, false);
865
+ if (isSourceCheckout) {
866
+ const specialistAudit = auditSpecialists({ rootDir: ROOT_DIR, silent: true });
867
+ add('Specialist/skill audit cross-checks', specialistAudit.pass, false);
868
+
869
+ // The catalog is the public-service source of truth; doctor joins it to runtime dispatch
870
+ // and the generated reference so a command that is unroutable or undocumented surfaces here.
871
+
872
+ const { buildCliServiceInventory } = await import('../lib/cli-service-inventory.mjs');
873
+ const cliDrift = buildCliServiceInventory({ rootDir: ROOT_DIR })
874
+ .filter((entry) => !entry.runnable || !entry.documented);
875
+ add('Public CLI dispatch/reference parity', cliDrift.length === 0, false);
876
+ } else {
877
+ add('Specialist/skill audit cross-checks (skipped — published package)', true, true);
878
+ add('Public CLI dispatch/reference parity (skipped — published package)', true, true);
879
+ }
863
880
  const { lintResearchRepo } = await import('../lib/research-lint.mjs');
864
881
  const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
865
882
  add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
@@ -1561,17 +1578,18 @@ async function cmdExport(args) {
1561
1578
  const outFlag = args.find((a) => a.startsWith('--output='))?.split('=')[1];
1562
1579
  const detectOnly = args.includes('--detect');
1563
1580
  const figures = !args.includes('--no-figures');
1581
+ const branding = args.includes('--plain') || args.includes('--no-brand') ? 'plain' : 'construct';
1564
1582
 
1565
1583
  const { detect, exportMarkdown, EXPORT_FORMATS } = await import('../lib/document-export.mjs');
1566
1584
 
1567
1585
  if (detectOnly) {
1568
1586
  const format = toFlag || 'pdf';
1569
- println(JSON.stringify(detect(format), null, 2));
1587
+ println(JSON.stringify(detect(format, process.env, { branding }), null, 2));
1570
1588
  return;
1571
1589
  }
1572
1590
 
1573
1591
  if (!inputPath) {
1574
- errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--figures|--no-figures] [--detect]');
1592
+ errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--figures|--no-figures] [--plain] [--detect]');
1575
1593
  process.exit(1);
1576
1594
  }
1577
1595
  if (!toFlag) {
@@ -1586,6 +1604,7 @@ async function cmdExport(args) {
1586
1604
  outputPath: resolvedOut,
1587
1605
  format: toFlag,
1588
1606
  figures: toFlag === 'pdf' ? figures : false,
1607
+ branding,
1589
1608
  repoRoot: ROOT_DIR,
1590
1609
  });
1591
1610
  if (!result.ok) {
@@ -2837,12 +2856,25 @@ async function cmdIntakeConfig(args) {
2837
2856
  const cwd = process.cwd();
2838
2857
 
2839
2858
  const action = args[0];
2859
+ if (action === 'migrate') {
2860
+ const { migrateLegacyIntakeConfig } = await import('../lib/intake/intake-config.mjs');
2861
+ const result = migrateLegacyIntakeConfig(cwd);
2862
+ if (!result.migrated) {
2863
+ info(result.reason ?? 'nothing to migrate');
2864
+ return;
2865
+ }
2866
+ info('Migrated .cx/intake-config.json → construct.config.json intakePolicy');
2867
+ println(JSON.stringify(result.policy, null, 2));
2868
+ return;
2869
+ }
2870
+
2840
2871
  if (!action || action === 'show') {
2841
2872
  const cfg = loadIntakeConfig(cwd, process.env);
2842
2873
  const desc = describeIntakeDepth(cfg.maxDepth);
2843
2874
  println(`Intake config (root: ${cwd})`);
2844
2875
  println('');
2845
2876
  println(` includeProjectInbox: ${cfg.includeProjectInbox ? 'on' : 'off'} (.cx/inbox)`);
2877
+ println(` includeRootInbox: ${(cfg.includeRootInbox ?? cfg.includeArchetypeInbox) ? 'on' : 'off'} (inbox/)`);
2846
2878
  println(` includeDocsIntake: ${cfg.includeDocsIntake ? 'on' : 'off'} (docs/intake)`);
2847
2879
  println(` maxDepth: ${cfg.maxDepth} — ${desc.label}`);
2848
2880
  println(` ${desc.detail}`);
@@ -2877,6 +2909,9 @@ async function cmdIntakeConfig(args) {
2877
2909
  const docsFlag = tail.find((a) => a.startsWith('--include-docs-intake='));
2878
2910
  if (docsFlag) patch.includeDocsIntake = /^(on|true|1|yes)$/i.test(docsFlag.split('=')[1] || '');
2879
2911
 
2912
+ const rootFlag = tail.find((a) => a.startsWith('--include-root-inbox='));
2913
+ if (rootFlag) patch.includeRootInbox = /^(on|true|1|yes)$/i.test(rootFlag.split('=')[1] || '');
2914
+
2880
2915
  const addFlags = tail.filter((a) => a.startsWith('--add-dir='));
2881
2916
  const removeFlags = tail.filter((a) => a.startsWith('--remove-dir='));
2882
2917
 
@@ -2903,6 +2938,123 @@ async function cmdIntakeConfig(args) {
2903
2938
  }
2904
2939
  }
2905
2940
 
2941
+ async function cmdSources(args) {
2942
+ const {
2943
+ validateSourceTargets,
2944
+ normalizeConfigTarget,
2945
+ resolveEffectiveSourceTargetsFromConfig,
2946
+ legacyEnvSourceTargets,
2947
+ SOURCE_PROVIDERS,
2948
+ } = await import('../lib/config/source-targets.mjs');
2949
+ const {
2950
+ loadProjectConfig,
2951
+ writeProjectConfig,
2952
+ findProjectConfigPath,
2953
+ PROJECT_CONFIG_FILENAME,
2954
+ } = await import('../lib/config/project-config.mjs');
2955
+
2956
+ const cwd = process.cwd();
2957
+ const action = args[0] ?? 'list';
2958
+
2959
+ if (action === 'list' || action === 'show') {
2960
+ const { config, path: cfgPath } = loadProjectConfig(cwd, process.env);
2961
+ const configTargets = (config.sources?.targets ?? []).map(normalizeConfigTarget);
2962
+ const envTargets = legacyEnvSourceTargets(process.env);
2963
+ const effective = resolveEffectiveSourceTargetsFromConfig(config, process.env);
2964
+ println(`Source targets (config: ${cfgPath ?? 'default'})`);
2965
+ println('');
2966
+ if (configTargets.length === 0) println(' (no targets in construct.config.json)');
2967
+ for (const t of configTargets) {
2968
+ println(` ${t.id} · ${t.provider} · ${JSON.stringify(t.selector)}`);
2969
+ }
2970
+ if (envTargets.length) {
2971
+ println('');
2972
+ println('Legacy env targets (merged at runtime):');
2973
+ for (const t of envTargets) {
2974
+ println(` ${t.id} · ${t.provenance} · ${t.provider} · ${JSON.stringify(t.selector)}`);
2975
+ }
2976
+ }
2977
+ println('');
2978
+ println(`Effective targets: ${effective.length}`);
2979
+ return;
2980
+ }
2981
+
2982
+ if (action === 'validate') {
2983
+ const loaded = loadProjectConfig(cwd, process.env);
2984
+ const targetErrors = validateSourceTargets(loaded.raw?.sources?.targets ?? loaded.config?.sources?.targets ?? []);
2985
+ const all = [...(loaded.errors ?? []), ...targetErrors];
2986
+ if (!all.length) {
2987
+ info(`${loaded.path ?? PROJECT_CONFIG_FILENAME}: source targets valid`);
2988
+ return;
2989
+ }
2990
+ errorln(`Source target validation failed (${all.length}):`);
2991
+ for (const e of all) errorln(` - ${e}`);
2992
+ process.exit(1);
2993
+ }
2994
+
2995
+ if (action === 'add') {
2996
+ const provider = args[1];
2997
+ const id = args[2];
2998
+ const selectorRaw = args[3];
2999
+ if (!provider || !id || !selectorRaw) {
3000
+ errorln('Usage: construct sources add <provider> <id> <selector-json>');
3001
+ process.exit(1);
3002
+ }
3003
+ if (!SOURCE_PROVIDERS.includes(provider)) {
3004
+ errorln(`Unknown provider: ${provider}. Expected: ${SOURCE_PROVIDERS.join(', ')}`);
3005
+ process.exit(1);
3006
+ }
3007
+ let selector;
3008
+ try { selector = JSON.parse(selectorRaw); } catch (err) {
3009
+ errorln(`Invalid selector JSON: ${err.message}`);
3010
+ process.exit(1);
3011
+ }
3012
+ const target = normalizeConfigTarget({ id, provider, selector });
3013
+ const targetErrors = validateSourceTargets([target]);
3014
+ if (targetErrors.length) {
3015
+ for (const e of targetErrors) errorln(` - ${e}`);
3016
+ process.exit(1);
3017
+ }
3018
+ const cfgPath = findProjectConfigPath(cwd) || path.join(cwd, PROJECT_CONFIG_FILENAME);
3019
+ const { config } = loadProjectConfig(cwd, process.env);
3020
+ const existing = config.sources?.targets ?? [];
3021
+ if (existing.some((t) => t.id === id)) {
3022
+ errorln(`Target id already exists: ${id}`);
3023
+ process.exit(1);
3024
+ }
3025
+ const next = {
3026
+ ...config,
3027
+ sources: { ...(config.sources ?? {}), targets: [...existing, target] },
3028
+ };
3029
+ writeProjectConfig(cfgPath, next);
3030
+ info(`added source target ${id}`);
3031
+ return;
3032
+ }
3033
+
3034
+ if (action === 'remove') {
3035
+ const id = args[1];
3036
+ if (!id) {
3037
+ errorln('Usage: construct sources remove <id>');
3038
+ process.exit(1);
3039
+ }
3040
+ const cfgPath = findProjectConfigPath(cwd) || path.join(cwd, PROJECT_CONFIG_FILENAME);
3041
+ const { config } = loadProjectConfig(cwd, process.env);
3042
+ const existing = config.sources?.targets ?? [];
3043
+ const nextTargets = existing.filter((t) => t.id !== id);
3044
+ if (nextTargets.length === existing.length) {
3045
+ errorln(`No target with id: ${id}`);
3046
+ process.exit(1);
3047
+ }
3048
+ const next = { ...config, sources: { ...(config.sources ?? {}), targets: nextTargets } };
3049
+ writeProjectConfig(cfgPath, next);
3050
+ info(`removed source target ${id}`);
3051
+ return;
3052
+ }
3053
+
3054
+ errorln(`Unknown sources action: ${action}. Available: list, add, remove, validate`);
3055
+ process.exit(1);
3056
+ }
3057
+
2906
3058
  async function cmdIntakeMetrics() {
2907
3059
  try {
2908
3060
  const { computeIntakeMetrics, pendingAge } = await import('../lib/embed/intake-metrics.mjs');
@@ -5400,6 +5552,7 @@ const handlers = new Map([
5400
5552
  ['status', cmdStatus],
5401
5553
  ['install', cmdInstall],
5402
5554
  ['config', cmdConfig],
5555
+ ['sources', cmdSources],
5403
5556
  ['intake', cmdIntake],
5404
5557
  ['recommendations', cmdRecommendations],
5405
5558
  ['integrations', cmdIntegrations],
@@ -5648,7 +5801,24 @@ const handlers = new Map([
5648
5801
  ['artifact', async (args) => {
5649
5802
  const sub = args[0];
5650
5803
  if (sub === 'validate') return runArtifactValidateCli(args.slice(1));
5651
- errorln(`Unknown artifact subcommand: ${sub || '(none)'}. Available: validate`);
5804
+ if (sub === 'workflow') {
5805
+ const rest = args.slice(1);
5806
+ const request = rest.filter((arg) => !arg.startsWith('--')).join(' ');
5807
+ const read = (name) => rest.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
5808
+ const { runArtifactWorkflow } = await import('../lib/artifact-workflow.mjs');
5809
+ const result = runArtifactWorkflow({
5810
+ input: request,
5811
+ artifactType: read('--type'),
5812
+ filePath: read('--file'),
5813
+ format: read('--to'),
5814
+ outputPath: read('--output'),
5815
+ branding: rest.includes('--plain') ? 'plain' : undefined,
5816
+ approvalMode: rest.includes('--apply') ? 'allow-durable-write' : (read('--approval-mode') || 'proposal-only'),
5817
+ }, { cwd: process.cwd(), rootDir: ROOT_DIR });
5818
+ println(JSON.stringify(result, null, 2));
5819
+ return;
5820
+ }
5821
+ errorln(`Unknown artifact subcommand: ${sub || '(none)'}. Available: validate, workflow`);
5652
5822
  process.exit(1);
5653
5823
  }],
5654
5824
  ['doc', async (args) => {
@@ -14,6 +14,12 @@ import { fileURLToPath } from 'node:url';
14
14
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
15
  const EMPTY_MANIFEST = { version: 1, artifacts: {} };
16
16
 
17
+ // Kept here, next to the manifest resolver, so every public workflow surface
18
+ // uses one answer for what can carry Construct styling. Source formats remain
19
+ // deliberately plain unless a caller explicitly asks otherwise.
20
+ export const BRAND_CAPABLE_FORMATS = Object.freeze(['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex']);
21
+ export const PLAIN_OUTPUT_FORMATS = Object.freeze(['txt', 'md', 'mdx']);
22
+
17
23
  let cached = null;
18
24
  let cachedRoot = null;
19
25
 
@@ -60,6 +66,140 @@ export function artifactTypes(opts = {}) {
60
66
  return Object.keys(manifest.artifacts ?? {});
61
67
  }
62
68
 
69
+ function isObject(value) {
70
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
71
+ }
72
+
73
+ function mergeObjects(...values) {
74
+ const out = {};
75
+ for (const value of values) {
76
+ if (!isObject(value)) continue;
77
+ for (const [key, next] of Object.entries(value)) {
78
+ out[key] = isObject(next) && isObject(out[key]) ? mergeObjects(out[key], next) : structuredClone(next);
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+
84
+ function unique(items) {
85
+ return [...new Set(items.filter(Boolean))];
86
+ }
87
+
88
+ /**
89
+ * Resolve a user-facing type or registered alias. The result intentionally
90
+ * distinguishes an unknown request from a fallback: callers must ask for
91
+ * classification/registration instead of silently turning it into a PRD.
92
+ */
93
+ export function resolveArtifactType(type, opts = {}) {
94
+ const requestedType = String(type ?? '').trim().toLowerCase();
95
+ const manifest = loadArtifactManifest(opts);
96
+ if (!requestedType) {
97
+ return {
98
+ status: 'unrecognized',
99
+ requestedType: null,
100
+ type: null,
101
+ guidance: 'Specify a registered document class or register one in specialists/artifact-manifest.json.',
102
+ };
103
+ }
104
+ for (const [registeredType, entry] of Object.entries(manifest.artifacts ?? {})) {
105
+ const aliases = [registeredType, ...(entry.aliases ?? [])].map((value) => String(value).toLowerCase());
106
+ if (aliases.includes(requestedType)) {
107
+ return { status: 'registered', requestedType, type: registeredType, entry };
108
+ }
109
+ }
110
+ return {
111
+ status: 'unrecognized',
112
+ requestedType,
113
+ type: null,
114
+ guidance: `Document class '${requestedType}' is not registered. Classify the request or add it to specialists/artifact-manifest.json.`,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Resolve the canonical workflow contract for a registered document class.
120
+ * Precedence is deliberately explicit: invocation > project config > manifest
121
+ * defaults. Existing entries remain compatible: primaryOwners and releaseGate
122
+ * are the fallback author and reviewer/validation declarations.
123
+ */
124
+ export function resolveArtifactWorkflowContract(type, {
125
+ rootDir,
126
+ cwd = process.cwd(),
127
+ projectConfig = null,
128
+ overrides = null,
129
+ } = {}) {
130
+ const resolved = resolveArtifactType(type, { rootDir, cwd });
131
+ if (resolved.status !== 'registered') return resolved;
132
+
133
+ const manifest = loadArtifactManifest({ rootDir, cwd });
134
+ const entry = resolved.entry;
135
+ const config = projectConfig?.artifactWorkflow ?? {};
136
+ const manifestDefaults = manifest.workflowDefaults ?? {};
137
+ const projectType = config.types?.[resolved.type] ?? {};
138
+ const appliedOverrides = [];
139
+ if (isObject(config.defaults) && Object.keys(config.defaults).length) appliedOverrides.push('project.defaults');
140
+ if (isObject(projectType) && Object.keys(projectType).length) appliedOverrides.push(`project.types.${resolved.type}`);
141
+ if (isObject(overrides) && Object.keys(overrides).length) appliedOverrides.push('invocation');
142
+
143
+ const configured = mergeObjects(manifestDefaults, entry, config.defaults, projectType, overrides);
144
+ const releaseGate = configured.releaseGate ?? {};
145
+ const authorChain = unique(configured.authorChain ?? configured.primaryOwners ?? []);
146
+ const reviewerChain = unique(configured.reviewerChain ?? [
147
+ ...(releaseGate.requiredReviewers ?? []),
148
+ ...(releaseGate.optionalReviewers ?? []),
149
+ ]);
150
+ const outputs = mergeObjects({
151
+ formats: [...BRAND_CAPABLE_FORMATS, ...PLAIN_OUTPUT_FORMATS],
152
+ branding: 'construct',
153
+ }, configured.outputs);
154
+ const validation = mergeObjects({ releaseGate: true }, configured.validation);
155
+
156
+ return {
157
+ status: 'registered',
158
+ requestedType: resolved.requestedType,
159
+ type: resolved.type,
160
+ documentClass: configured.documentClass ?? resolved.type,
161
+ template: configured.template ?? null,
162
+ workflowSkill: configured.workflowSkill ?? null,
163
+ authorChain,
164
+ reviewerChain,
165
+ requiredReviewers: unique(releaseGate.requiredReviewers ?? []),
166
+ optionalReviewers: unique(releaseGate.optionalReviewers ?? []),
167
+ researchProfile: configured.researchProfile ?? null,
168
+ validation,
169
+ releaseGate,
170
+ outputs,
171
+ appliedOverrides,
172
+ };
173
+ }
174
+
175
+ /** A dependency-free validator used by startup checks and tests. */
176
+ export function validateArtifactManifest(manifest) {
177
+ const errors = [];
178
+ if (!isObject(manifest)) return { valid: false, errors: ['root: must be an object'] };
179
+ if (!Number.isInteger(manifest.version) || manifest.version < 1) errors.push('version: must be an integer >= 1');
180
+ if (!isObject(manifest.artifacts)) errors.push('artifacts: must be an object');
181
+ for (const [type, entry] of Object.entries(manifest.artifacts ?? {})) {
182
+ const prefix = `artifacts.${type}`;
183
+ if (!isObject(entry)) { errors.push(`${prefix}: must be an object`); continue; }
184
+ if (typeof entry.template !== 'string' || !entry.template) errors.push(`${prefix}.template: required string missing`);
185
+ if (!Array.isArray(entry.primaryOwners) || entry.primaryOwners.some((owner) => typeof owner !== 'string')) {
186
+ errors.push(`${prefix}.primaryOwners: required string array missing`);
187
+ }
188
+ for (const field of ['authorChain', 'reviewerChain', 'aliases']) {
189
+ if (entry[field] !== undefined && (!Array.isArray(entry[field]) || entry[field].some((value) => typeof value !== 'string'))) {
190
+ errors.push(`${prefix}.${field}: must be an array of strings`);
191
+ }
192
+ }
193
+ if (entry.outputs?.formats !== undefined && (!Array.isArray(entry.outputs.formats) || entry.outputs.formats.length === 0)) {
194
+ errors.push(`${prefix}.outputs.formats: must be a non-empty array`);
195
+ }
196
+ if (entry.outputs?.branding !== undefined && !['construct', 'plain'].includes(entry.outputs.branding)) {
197
+ errors.push(`${prefix}.outputs.branding: must be construct or plain`);
198
+ }
199
+ }
200
+ return errors.length ? { valid: false, errors } : { valid: true, errors: [] };
201
+ }
202
+
63
203
  export function structureRequirementsFromManifest(opts = {}) {
64
204
  const manifest = loadArtifactManifest(opts);
65
205
  const out = {};
@@ -97,16 +237,22 @@ export function resolveToneForArtifact(type, { cwd = process.cwd(), rootDir } =
97
237
  }
98
238
 
99
239
  export function templateMetadata(type, { cwd = process.cwd(), rootDir } = {}) {
100
- const entry = getArtifactEntry(type, { rootDir, cwd });
101
- if (!entry) return null;
240
+ const workflow = resolveArtifactWorkflowContract(type, { rootDir, cwd });
241
+ if (workflow.status !== 'registered') return null;
242
+ const entry = getArtifactEntry(workflow.type, { rootDir, cwd });
102
243
  return {
103
- type,
104
- tone: resolveToneForArtifact(type, { cwd, rootDir }),
244
+ type: workflow.type,
245
+ documentClass: workflow.documentClass,
246
+ tone: resolveToneForArtifact(workflow.type, { cwd, rootDir }),
105
247
  toneAllowed: entry.toneAllowed ?? [],
106
248
  structureRequirements: entry.structureRequirements ?? [],
107
249
  visualRequirements: entry.visualRequirements ?? [],
108
250
  primaryOwners: entry.primaryOwners ?? [],
109
251
  workflowSkill: entry.workflowSkill ?? null,
110
252
  releaseGate: entry.releaseGate ?? null,
253
+ authorChain: workflow.authorChain,
254
+ reviewerChain: workflow.reviewerChain,
255
+ validation: workflow.validation,
256
+ outputs: workflow.outputs,
111
257
  };
112
258
  }