@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 +2 -1
- package/bin/construct +180 -10
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/auto-docs.mjs +3 -1
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +20 -3
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +50 -1
- package/lib/config/source-targets.mjs +311 -0
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/document-export.mjs +38 -16
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +2 -1
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/export-branding.mjs +32 -0
- package/lib/hooks/session-start.mjs +14 -11
- package/lib/init-unified.mjs +24 -23
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- package/lib/mcp/server.mjs +19 -1
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/playwright-demo.mjs +2 -0
- package/lib/server/index.mjs +31 -2
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- package/package.json +1 -1
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/templates/distribution/construct-reference.docx +0 -0
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` |
|
|
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
|
|
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
|
-
|
|
511
|
-
|
|
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
|
-
|
|
862
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
if (
|
|
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
|
-
|
|
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
|
}
|