@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
|
@@ -19,6 +19,11 @@ import { homedir } from 'node:os';
|
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
import { loadConstructEnv } from '../env-config.mjs';
|
|
21
21
|
import { addObservation } from '../observation-store.mjs';
|
|
22
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
23
|
+
import {
|
|
24
|
+
resolveEffectiveSourceTargetsFromConfig,
|
|
25
|
+
resolveKnownSourcesFromTargets,
|
|
26
|
+
} from '../config/source-targets.mjs';
|
|
22
27
|
|
|
23
28
|
// ─── Self-query detection ────────────────────────────────────────────────────
|
|
24
29
|
|
|
@@ -62,7 +67,13 @@ function isSelfQuery(query) {
|
|
|
62
67
|
* @param {object} env
|
|
63
68
|
* @returns {{ id: string, provider: string, ref: string, display: string }[]}
|
|
64
69
|
*/
|
|
65
|
-
export function resolveKnownSources(env = process.env) {
|
|
70
|
+
export function resolveKnownSources(env = process.env, cwd = process.cwd()) {
|
|
71
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
72
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
|
|
73
|
+
if (targets.length > 0) {
|
|
74
|
+
return resolveKnownSourcesFromTargets(targets);
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
const sources = [];
|
|
67
78
|
|
|
68
79
|
// GitHub repos
|
|
@@ -112,8 +123,8 @@ export function resolveKnownSources(env = process.env) {
|
|
|
112
123
|
* @param {object} env
|
|
113
124
|
* @returns {{ provider: string, ref: string, display: string } | null}
|
|
114
125
|
*/
|
|
115
|
-
export function matchSourceFromQuery(query, env = process.env) {
|
|
116
|
-
const sources = resolveKnownSources(env);
|
|
126
|
+
export function matchSourceFromQuery(query, env = process.env, cwd = process.cwd()) {
|
|
127
|
+
const sources = resolveKnownSources(env, cwd);
|
|
117
128
|
const q = query.toLowerCase();
|
|
118
129
|
|
|
119
130
|
// Exact or substring match — longest id wins
|
|
@@ -138,13 +149,12 @@ export function matchSourceFromQuery(query, env = process.env) {
|
|
|
138
149
|
* @param {object} [opts.env] - Env override
|
|
139
150
|
* @returns {Promise<DemandFetchResult>}
|
|
140
151
|
*/
|
|
141
|
-
export async function demandFetch({ query, rootDir, env } = {}) {
|
|
152
|
+
export async function demandFetch({ query, rootDir, env, cwd } = {}) {
|
|
142
153
|
const resolvedEnv = env ?? loadConstructEnv();
|
|
143
154
|
const root = rootDir ?? (resolvedEnv.CX_DATA_DIR?.trim() || homedir());
|
|
155
|
+
const projectRoot = cwd ?? root;
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
// always fetch from that provider — even if isSelfQuery() also fires.
|
|
147
|
-
const match = matchSourceFromQuery(query, resolvedEnv);
|
|
157
|
+
const match = matchSourceFromQuery(query, resolvedEnv, projectRoot);
|
|
148
158
|
|
|
149
159
|
// Self-referential queries about Construct itself → route to knowledge_search,
|
|
150
160
|
// but only when no external provider source was matched.
|
|
@@ -168,8 +178,7 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
if (!match) {
|
|
171
|
-
|
|
172
|
-
return demandFetchAll({ query, rootDir: root, env: resolvedEnv });
|
|
181
|
+
return demandFetchAll({ query, rootDir: root, env: resolvedEnv, cwd: projectRoot });
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
// Dynamically import provider registry to avoid loading all providers at module init
|
|
@@ -188,7 +197,7 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
// Build read calls for the matched source and execute them
|
|
191
|
-
const readCalls = buildReadCalls(match, resolvedEnv);
|
|
200
|
+
const readCalls = buildReadCalls(match, resolvedEnv, projectRoot);
|
|
192
201
|
if (!readCalls.length) {
|
|
193
202
|
return {
|
|
194
203
|
ok: false,
|
|
@@ -281,23 +290,32 @@ export async function demandFetch({ query, rootDir, env } = {}) {
|
|
|
281
290
|
* @param {object} [opts.env] - Resolved env
|
|
282
291
|
* @returns {Promise<DemandFetchResult>}
|
|
283
292
|
*/
|
|
284
|
-
async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
293
|
+
async function demandFetchAll({ query, rootDir, env, cwd } = {}) {
|
|
285
294
|
const { ProviderRegistry } = await import('./providers/registry.mjs');
|
|
286
295
|
const registry = await ProviderRegistry.fromEnv(env);
|
|
287
|
-
const
|
|
296
|
+
const projectRoot = cwd ?? rootDir ?? process.cwd();
|
|
297
|
+
const { config } = loadProjectConfig(projectRoot, env);
|
|
298
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
|
|
299
|
+
const scoped = targets.length > 0;
|
|
300
|
+
|
|
301
|
+
const providerNames = scoped
|
|
302
|
+
? [...new Set(targets.map((t) => t.provider))].filter((name) => registry.get(name))
|
|
303
|
+
: registry.names();
|
|
288
304
|
|
|
289
305
|
if (!providerNames.length) {
|
|
290
306
|
return {
|
|
291
307
|
ok: false,
|
|
292
308
|
reason: 'no_providers',
|
|
293
|
-
message:
|
|
309
|
+
message: scoped
|
|
310
|
+
? 'No provider credentials configured for the declared source targets'
|
|
311
|
+
: 'No provider credentials configured in config.env',
|
|
294
312
|
items: [],
|
|
295
313
|
};
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
const allItems = [];
|
|
299
317
|
const errors = [];
|
|
300
|
-
const sources = resolveKnownSources(env);
|
|
318
|
+
const sources = resolveKnownSources(env, projectRoot);
|
|
301
319
|
|
|
302
320
|
// Group known sources by provider so we can batch-fetch per-provider
|
|
303
321
|
const byProvider = new Map();
|
|
@@ -306,11 +324,16 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
306
324
|
byProvider.get(src.provider).push(src);
|
|
307
325
|
}
|
|
308
326
|
|
|
309
|
-
// Also include providers that have no explicit sources (Jira, Linear default fetch)
|
|
310
327
|
for (const name of providerNames) {
|
|
311
328
|
if (!byProvider.has(name)) byProvider.set(name, []);
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
if (scoped) {
|
|
332
|
+
for (const key of [...byProvider.keys()]) {
|
|
333
|
+
if (!providerNames.includes(key)) byProvider.delete(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
314
337
|
for (const [providerName, provSources] of byProvider) {
|
|
315
338
|
const provider = registry.get(providerName);
|
|
316
339
|
if (!provider) continue;
|
|
@@ -318,10 +341,12 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
318
341
|
// Build read calls — one set per known source, or a default if none listed
|
|
319
342
|
const matchList = provSources.length > 0
|
|
320
343
|
? provSources
|
|
321
|
-
: [{ provider: providerName, ref: null, display: providerName }];
|
|
344
|
+
: (scoped ? [] : [{ provider: providerName, ref: null, display: providerName }]);
|
|
345
|
+
|
|
346
|
+
if (!matchList.length) continue;
|
|
322
347
|
|
|
323
348
|
for (const src of matchList) {
|
|
324
|
-
const readCalls = buildReadCalls(src, env);
|
|
349
|
+
const readCalls = buildReadCalls(src, env, projectRoot);
|
|
325
350
|
if (!readCalls.length) continue;
|
|
326
351
|
for (const { ref, opts, display } of readCalls) {
|
|
327
352
|
try {
|
|
@@ -384,7 +409,7 @@ async function demandFetchAll({ query, rootDir, env } = {}) {
|
|
|
384
409
|
* @param {object} env
|
|
385
410
|
* @returns {{ ref: string, opts: object, display: string }[]}
|
|
386
411
|
*/
|
|
387
|
-
function buildReadCalls(match, env) {
|
|
412
|
+
function buildReadCalls(match, env, cwd = process.cwd()) {
|
|
388
413
|
if (match.provider === 'github') {
|
|
389
414
|
const opts = { repos: [match.ref], limit: 25 };
|
|
390
415
|
return [
|
|
@@ -397,7 +422,13 @@ function buildReadCalls(match, env) {
|
|
|
397
422
|
];
|
|
398
423
|
}
|
|
399
424
|
if (match.provider === 'jira') {
|
|
400
|
-
const
|
|
425
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
426
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env)
|
|
427
|
+
.filter((t) => t.provider === 'jira')
|
|
428
|
+
.map((t) => t.selector.project);
|
|
429
|
+
const projects = targets.length
|
|
430
|
+
? targets
|
|
431
|
+
: (env.JIRA_PROJECTS ?? '').split(',').map((p) => p.trim()).filter(Boolean);
|
|
401
432
|
const recencyDays = parseInt(env.JIRA_FETCH_RECENCY_DAYS, 10) || 30;
|
|
402
433
|
|
|
403
434
|
let jql;
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -148,6 +148,12 @@ export function resolveInboxDirs(rootDir, env = process.env) {
|
|
|
148
148
|
const config = loadIntakeConfig(rootDir, env);
|
|
149
149
|
const dirs = [];
|
|
150
150
|
|
|
151
|
+
if (config.includeRootInbox ?? config.includeArchetypeInbox) {
|
|
152
|
+
const rootInbox = join(rootDir, ARCHETYPE_INBOX_SUBDIR);
|
|
153
|
+
mkdirSync(rootInbox, { recursive: true });
|
|
154
|
+
dirs.push(rootInbox);
|
|
155
|
+
}
|
|
156
|
+
|
|
151
157
|
if (config.includeProjectInbox) {
|
|
152
158
|
const projectInbox = join(rootDir, DEFAULT_INBOX_SUBDIR);
|
|
153
159
|
mkdirSync(projectInbox, { recursive: true });
|
|
@@ -159,16 +165,6 @@ export function resolveInboxDirs(rootDir, env = process.env) {
|
|
|
159
165
|
if (existsSync(docsIntake)) dirs.push(docsIntake);
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
// Archetype inbox is a profile-driven drop zone for raw source files
|
|
163
|
-
// (capabilities.intake.inbox). Off by default; init flips it on when the
|
|
164
|
-
// active profile declares the capability so non-archetype projects keep
|
|
165
|
-
// their pre-Piece-C behavior.
|
|
166
|
-
|
|
167
|
-
if (config.includeArchetypeInbox) {
|
|
168
|
-
const archetypeInbox = join(rootDir, ARCHETYPE_INBOX_SUBDIR);
|
|
169
|
-
if (existsSync(archetypeInbox)) dirs.push(archetypeInbox);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
168
|
for (const candidate of config.parentDirs) {
|
|
173
169
|
if (!candidate) continue;
|
|
174
170
|
if (!existsSync(candidate)) continue;
|
package/lib/embed/worker.mjs
CHANGED
|
@@ -28,8 +28,9 @@ if (configPath) {
|
|
|
28
28
|
} else {
|
|
29
29
|
const { ProviderRegistry } = await import('./providers/registry.mjs');
|
|
30
30
|
const { EMPTY_CONFIG } = await import('./config.mjs');
|
|
31
|
+
const { resolveAutoEmbedSources } = await import('./auto-sources.mjs');
|
|
31
32
|
const registry = await ProviderRegistry.fromEnv(env);
|
|
32
|
-
const sources =
|
|
33
|
+
const sources = resolveAutoEmbedSources({ cwd: process.cwd(), env, registry });
|
|
33
34
|
daemonOpts.registry = registry;
|
|
34
35
|
daemonOpts.config = {
|
|
35
36
|
...EMPTY_CONFIG,
|
|
@@ -13,6 +13,7 @@ import { recommendPlan as recommendPlanCore } from './triage.mjs';
|
|
|
13
13
|
import { invokeWorkflow as invokeWorkflowCore } from './workflow-invoke.mjs';
|
|
14
14
|
import { buildCapabilityContract } from './capability.mjs';
|
|
15
15
|
import { resolveExecution as resolveExecutionCore } from './execution.mjs';
|
|
16
|
+
import { planArtifactWorkflow as planArtifactWorkflowCore, runArtifactWorkflow as runArtifactWorkflowCore } from '../artifact-workflow.mjs';
|
|
16
17
|
|
|
17
18
|
export { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION, isClientCompatible } from './contract-version.mjs';
|
|
18
19
|
export { APPROVAL_MODES } from './audit.mjs';
|
|
@@ -85,3 +86,13 @@ export function describeCapabilities({ env, cwd, rootDir } = {}) {
|
|
|
85
86
|
export function resolveExecution(request = {}, { env, cwd, registryPath } = {}) {
|
|
86
87
|
return wrapContractResult(resolveExecutionCore(request, { env, cwd, registryPath }), { surface: 'sdk', env, cwd });
|
|
87
88
|
}
|
|
89
|
+
|
|
90
|
+
/** Plan a manifest-backed artifact workflow without executing specialists. */
|
|
91
|
+
export function planArtifactWorkflow(request = {}, { env, cwd, rootDir } = {}) {
|
|
92
|
+
return wrapContractResult(planArtifactWorkflowCore(request, { cwd, rootDir }), { surface: 'sdk', env, cwd });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Run only locally observable artifact steps and return truthful provenance. */
|
|
96
|
+
export function runArtifactWorkflow(request = {}, { env, cwd, rootDir } = {}) {
|
|
97
|
+
return wrapContractResult(runArtifactWorkflowCore(request, { cwd, rootDir }), { surface: 'sdk', env, cwd });
|
|
98
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/export-branding.mjs — one honest branding contract for distribution exports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BRAND_CAPABLE_FORMATS, PLAIN_OUTPUT_FORMATS } from './artifact-manifest.mjs';
|
|
6
|
+
|
|
7
|
+
export const EXPORT_BRANDING_CAPABILITIES = Object.freeze({
|
|
8
|
+
pdf: { capable: true, mechanism: 'typst-template', asset: 'construct-pdf.typ' },
|
|
9
|
+
html: { capable: true, mechanism: 'pandoc-template', asset: 'construct-web.html' },
|
|
10
|
+
deck: { capable: true, mechanism: 'pandoc-template', asset: 'construct-deck.html' },
|
|
11
|
+
pptx: { capable: true, mechanism: 'native-pptx', asset: 'pptxgenjs' },
|
|
12
|
+
docx: { capable: true, mechanism: 'reference-doc', asset: 'construct-reference.docx' },
|
|
13
|
+
doc: { capable: true, mechanism: 'docx-reference-via-libreoffice', asset: 'construct-reference.docx' },
|
|
14
|
+
rtf: { capable: true, mechanism: 'pandoc-writer', asset: null },
|
|
15
|
+
odt: { capable: true, mechanism: 'pandoc-writer', asset: null },
|
|
16
|
+
epub: { capable: true, mechanism: 'pandoc-writer', asset: null },
|
|
17
|
+
tex: { capable: true, mechanism: 'pandoc-writer', asset: null },
|
|
18
|
+
txt: { capable: false, mechanism: 'plain-source', asset: null },
|
|
19
|
+
md: { capable: false, mechanism: 'plain-source', asset: null },
|
|
20
|
+
mdx: { capable: false, mechanism: 'plain-source', asset: null },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function resolveExportBranding(format, requested = 'construct') {
|
|
24
|
+
const capability = EXPORT_BRANDING_CAPABILITIES[format];
|
|
25
|
+
if (!capability) return { requested, applied: 'none', capable: false, reason: 'unsupported format' };
|
|
26
|
+
if (!BRAND_CAPABLE_FORMATS.includes(format) || PLAIN_OUTPUT_FORMATS.includes(format)) {
|
|
27
|
+
return { requested, applied: 'none', capable: false, reason: 'format has no styling surface' };
|
|
28
|
+
}
|
|
29
|
+
if (requested === 'plain') return { requested, applied: 'plain', capable: true, mechanism: capability.mechanism, reason: 'explicit opt-out' };
|
|
30
|
+
return { requested: 'construct', applied: 'construct', capable: true, mechanism: capability.mechanism, asset: capability.asset, reason: 'default policy' };
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -265,28 +265,31 @@ const selfKnowledgeNote = '\nSelf-knowledge: call `knowledge_search` for any Con
|
|
|
265
265
|
let sourcesNote = '';
|
|
266
266
|
let embedStatusNote = '';
|
|
267
267
|
try {
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
const
|
|
268
|
+
const { loadProjectConfig } = await import('../config/project-config.mjs');
|
|
269
|
+
const { resolveEffectiveSourceTargetsFromConfig } = await import('../config/source-targets.mjs');
|
|
270
|
+
const { config } = loadProjectConfig(process.cwd(), process.env);
|
|
271
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, process.env);
|
|
272
|
+
const repos = targets.filter((t) => t.provider === 'github').map((t) => t.selector.repo);
|
|
273
|
+
const jiraTargets = targets.filter((t) => t.provider === 'jira');
|
|
274
|
+
const linearTargets = targets.filter((t) => t.provider === 'linear');
|
|
275
|
+
const slackTargets = targets.filter((t) => t.provider === 'slack');
|
|
276
|
+
const hasSources = targets.length > 0;
|
|
272
277
|
if (hasSources) {
|
|
273
278
|
const parts = [];
|
|
274
279
|
if (repos.length > 0) parts.push(`GitHub repos: ${repos.join(', ')}`);
|
|
275
|
-
if (
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
// tool for questions about them. The "fetch first, don't answer from
|
|
279
|
-
// memory" policy lives in the persona.
|
|
280
|
+
if (jiraTargets.length > 0) parts.push(`Jira projects: ${jiraTargets.map((t) => t.selector.project).join(', ')}`);
|
|
281
|
+
if (linearTargets.length > 0) parts.push(`Linear teams: ${linearTargets.map((t) => t.selector.team).join(', ')}`);
|
|
282
|
+
if (slackTargets.length > 0) parts.push(`Slack channels: ${slackTargets.map((t) => t.selector.channel).join(', ')}`);
|
|
280
283
|
sourcesNote = '\nProvider sources wired: ' + parts.join(' · ') +
|
|
281
284
|
'. Use `provider_fetch` for any question about them.\n';
|
|
282
285
|
}
|
|
286
|
+
} catch { /* best effort */ }
|
|
283
287
|
|
|
284
|
-
|
|
288
|
+
try {
|
|
285
289
|
const { resolveEmbedStatus, autoStartEmbedIfNeeded } = await import('../embed/cli.mjs');
|
|
286
290
|
const embedStatus = resolveEmbedStatus(process.env);
|
|
287
291
|
if (embedStatus.level !== 'none') {
|
|
288
292
|
embedStatusNote = `\n## Embed daemon\n${embedStatus.label} · ${embedStatus.detail}\n`;
|
|
289
|
-
// Auto-start if CX_AUTO_EMBED=1 and daemon is stopped
|
|
290
293
|
if (embedStatus.level === 'stopped' && process.env.CX_AUTO_EMBED === '1') {
|
|
291
294
|
const result = await autoStartEmbedIfNeeded(process.env);
|
|
292
295
|
if (result.started) {
|
package/lib/init-unified.mjs
CHANGED
|
@@ -886,21 +886,18 @@ async function main() {
|
|
|
886
886
|
created.push(".cx/inbox/");
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
// clobbering a user's pipeline above.
|
|
895
|
-
|
|
896
|
-
if (intakeCap?.inbox && !inboxDecision.skip) {
|
|
889
|
+
// Universal project-root inbox/ drop zone. Raw drops stay out of source via
|
|
890
|
+
// host-disposition IGNORED_PATTERNS (ADR-0027 §1). Skipped when custom intake
|
|
891
|
+
// pipeline already owns the project root.
|
|
892
|
+
|
|
893
|
+
if (!inboxDecision.skip) {
|
|
897
894
|
const projectInbox = path.join(target, 'inbox');
|
|
898
895
|
if (!fs.existsSync(projectInbox)) {
|
|
899
896
|
fs.mkdirSync(projectInbox, { recursive: true });
|
|
900
897
|
created.push('inbox/');
|
|
901
898
|
}
|
|
902
899
|
|
|
903
|
-
if (intakeCap
|
|
900
|
+
if (intakeCap?.dedup === 'sha256') {
|
|
904
901
|
const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
|
|
905
902
|
const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
|
|
906
903
|
if (!manifestExists) {
|
|
@@ -908,8 +905,12 @@ async function main() {
|
|
|
908
905
|
created.push(MANIFEST_REL_PATH);
|
|
909
906
|
}
|
|
910
907
|
}
|
|
908
|
+
} else {
|
|
909
|
+
skipped.push('inbox/ (deferred to existing intake)');
|
|
911
910
|
}
|
|
912
911
|
|
|
912
|
+
// Legacy archetype block removed — root inbox/ is universal (construct-1arm).
|
|
913
|
+
|
|
913
914
|
// Gitignore every Construct-generated artifact whose disposition is `ignored`
|
|
914
915
|
// (ADR-0027 §1): the six adapter dirs, the `.construct/` launcher, `.cx/`
|
|
915
916
|
// runtime state, and the generated config files all carry machine-specific
|
|
@@ -1080,26 +1081,26 @@ async function main() {
|
|
|
1080
1081
|
// .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
|
|
1081
1082
|
|
|
1082
1083
|
const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
|
|
1083
|
-
// When a custom intake surface was detected, default includeProjectInbox
|
|
1084
|
-
// to false so the watcher does not double-claim with the user's existing
|
|
1085
|
-
// pipeline. User can opt in later via `construct config intake.includeProjectInbox=true`.
|
|
1086
1084
|
|
|
1087
1085
|
if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
|
|
1088
1086
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
// existing includeProjectInbox / includeDocsIntake toggles rather than
|
|
1092
|
-
// polluting parentDirs (which is reserved for user-explicit dirs).
|
|
1087
|
+
intakeConfig.includeRootInbox = !inboxDecision.skip;
|
|
1088
|
+
intakeConfig.includeArchetypeInbox = intakeConfig.includeRootInbox;
|
|
1093
1089
|
|
|
1094
|
-
|
|
1095
|
-
intakeConfig.includeArchetypeInbox = true;
|
|
1096
|
-
}
|
|
1097
|
-
const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
|
|
1090
|
+
const { saveIntakePolicy } = await import('./config/intake-policy.mjs');
|
|
1098
1091
|
try {
|
|
1099
|
-
|
|
1100
|
-
|
|
1092
|
+
saveIntakePolicy(target, {
|
|
1093
|
+
maxDepth: intakeConfig.maxDepth,
|
|
1094
|
+
additionalDirs: intakeConfig.parentDirs ?? [],
|
|
1095
|
+
zones: {
|
|
1096
|
+
rootInbox: intakeConfig.includeRootInbox !== false,
|
|
1097
|
+
projectInbox: intakeConfig.includeProjectInbox !== false,
|
|
1098
|
+
docsIntake: intakeConfig.includeDocsIntake !== false,
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
created.push('construct.config.json (intakePolicy)');
|
|
1101
1102
|
} catch (err) {
|
|
1102
|
-
console.warn(`⚠️ Could not write intake
|
|
1103
|
+
console.warn(`⚠️ Could not write intake policy: ${err.message}`);
|
|
1103
1104
|
}
|
|
1104
1105
|
|
|
1105
1106
|
// Ask about documentation system
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/constants.mjs — shared intake watcher depth constants and guidance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const INTAKE_DEFAULT_MAX_DEPTH = 4;
|
|
6
|
+
export const INTAKE_HARD_MAX_DEPTH = 16;
|
|
7
|
+
|
|
8
|
+
export const INTAKE_DEPTH_GUIDANCE = [
|
|
9
|
+
{ value: 0, label: 'Only this directory', detail: 'Scans files directly in the parent dir, ignores all subdirs.' },
|
|
10
|
+
{ value: 1, label: 'One level deep', detail: 'Parent dir plus its immediate subdirs (e.g. parent/intake/file.md).' },
|
|
11
|
+
{ value: 2, label: 'Two levels deep', detail: 'Parent and two subdirs. A reasonable default for organized intake roots.' },
|
|
12
|
+
{ value: 4, label: 'Four levels (default)', detail: 'Catches most nested layouts without scanning huge trees.' },
|
|
13
|
+
{ value: 8, label: 'Deep scan', detail: 'Useful for archives. Slower; skip if the parent contains build output.' },
|
|
14
|
+
{ value: INTAKE_HARD_MAX_DEPTH, label: 'Unlimited (capped)', detail: `Walks up to ${INTAKE_HARD_MAX_DEPTH} levels — effectively unlimited. May be slow.` },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function describeIntakeDepth(depth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
18
|
+
const n = Number(depth);
|
|
19
|
+
const value = !Number.isFinite(n) || n < 0
|
|
20
|
+
? INTAKE_DEFAULT_MAX_DEPTH
|
|
21
|
+
: Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
22
|
+
const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
|
|
23
|
+
if (exact) return exact;
|
|
24
|
+
return {
|
|
25
|
+
value,
|
|
26
|
+
label: `Custom depth (${value})`,
|
|
27
|
+
detail: `Walks up to ${value} levels of subdirectories below each parent.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,136 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/intake/intake-config.mjs —
|
|
2
|
+
* lib/intake/intake-config.mjs — intake watcher config facade.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `<rootDir>/.cx/intake-config.json` and merges with env-driven defaults so
|
|
7
|
-
* CLI, dashboard, and process env all converge on the same answer.
|
|
8
|
-
*
|
|
9
|
-
* Schema:
|
|
10
|
-
* {
|
|
11
|
-
* parentDirs: string[] // absolute or rootDir-relative
|
|
12
|
-
* maxDepth: number // 0 = only the parent dir, no subdirs
|
|
13
|
-
* includeProjectInbox: boolean // always include <rootDir>/.cx/inbox
|
|
14
|
-
* includeDocsIntake: boolean // include <rootDir>/docs/intake when present
|
|
15
|
-
* }
|
|
16
|
-
*
|
|
17
|
-
* maxDepth guidance (surfaced verbatim in the dashboard):
|
|
18
|
-
* 0 — only files directly inside the parent dir
|
|
19
|
-
* 1 — parent dir + its immediate subdirs
|
|
20
|
-
* 2 — two levels of subdirs (a common default for project intake roots)
|
|
21
|
-
* 3+ — deep scans; use with care on large trees
|
|
4
|
+
* Reads and writes intake policy via construct.config.json intakePolicy.
|
|
5
|
+
* Legacy .cx/intake-config.json remains a warned compatibility fallback.
|
|
22
6
|
*/
|
|
23
7
|
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
26
8
|
import { shouldCreateCx } from '../project-detection.mjs';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
9
|
+
import {
|
|
10
|
+
loadIntakePolicy,
|
|
11
|
+
resolvedIntakeConfig,
|
|
12
|
+
saveIntakePolicy,
|
|
13
|
+
migrateLegacyIntakeConfig,
|
|
14
|
+
DEFAULT_INTAKE_POLICY,
|
|
15
|
+
} from '../config/intake-policy.mjs';
|
|
16
|
+
import {
|
|
17
|
+
INTAKE_DEFAULT_MAX_DEPTH,
|
|
18
|
+
INTAKE_HARD_MAX_DEPTH,
|
|
19
|
+
INTAKE_DEPTH_GUIDANCE,
|
|
20
|
+
describeIntakeDepth,
|
|
21
|
+
} from './constants.mjs';
|
|
22
|
+
import { INTAKE_CONFIG_FILE } from './legacy-paths.mjs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
INTAKE_DEFAULT_MAX_DEPTH,
|
|
27
|
+
INTAKE_HARD_MAX_DEPTH,
|
|
28
|
+
INTAKE_DEPTH_GUIDANCE,
|
|
29
|
+
describeIntakeDepth,
|
|
30
|
+
INTAKE_CONFIG_FILE,
|
|
31
|
+
migrateLegacyIntakeConfig,
|
|
32
|
+
DEFAULT_INTAKE_POLICY,
|
|
33
|
+
};
|
|
41
34
|
|
|
42
35
|
export const DEFAULT_INTAKE_CONFIG = Object.freeze({
|
|
43
36
|
parentDirs: [],
|
|
44
37
|
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
45
38
|
includeProjectInbox: true,
|
|
46
39
|
includeDocsIntake: true,
|
|
47
|
-
includeArchetypeInbox:
|
|
40
|
+
includeArchetypeInbox: true,
|
|
41
|
+
includeRootInbox: true,
|
|
48
42
|
});
|
|
49
43
|
|
|
50
44
|
export function intakeConfigPath(rootDir) {
|
|
51
45
|
return join(rootDir, INTAKE_CONFIG_FILE);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
function clampDepth(value) {
|
|
55
|
-
const n = Number(value);
|
|
56
|
-
if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
|
|
57
|
-
return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normalizeDir(dir, rootDir) {
|
|
61
|
-
if (typeof dir !== 'string') return null;
|
|
62
|
-
const trimmed = dir.trim();
|
|
63
|
-
if (!trimmed) return null;
|
|
64
|
-
return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseEnvDirs(env) {
|
|
68
|
-
const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
|
|
69
|
-
if (!raw) return [];
|
|
70
|
-
return raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
48
|
export function loadIntakeConfig(rootDir, env = process.env) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try { stored = JSON.parse(readFileSync(file, 'utf8')); } catch { stored = {}; }
|
|
49
|
+
const cfg = resolvedIntakeConfig(rootDir, env);
|
|
50
|
+
if (cfg.legacyWarning && process.stderr.isTTY) {
|
|
51
|
+
process.stderr.write(`[intake-config] ${cfg.legacyWarning}\n`);
|
|
78
52
|
}
|
|
79
|
-
|
|
80
|
-
const parentDirs = []
|
|
81
|
-
.concat(Array.isArray(stored.parentDirs) ? stored.parentDirs : [])
|
|
82
|
-
.concat(parseEnvDirs(env))
|
|
83
|
-
.map((dir) => normalizeDir(dir, rootDir))
|
|
84
|
-
.filter(Boolean);
|
|
85
|
-
|
|
86
|
-
const seen = new Set();
|
|
87
|
-
const uniqueDirs = [];
|
|
88
|
-
for (const dir of parentDirs) {
|
|
89
|
-
if (seen.has(dir)) continue;
|
|
90
|
-
seen.add(dir);
|
|
91
|
-
uniqueDirs.push(dir);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const envDepth = env?.CX_INTAKE_MAX_DEPTH;
|
|
95
|
-
const maxDepth = clampDepth(envDepth ?? stored.maxDepth ?? DEFAULT_INTAKE_CONFIG.maxDepth);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
parentDirs: uniqueDirs,
|
|
99
|
-
maxDepth,
|
|
100
|
-
includeProjectInbox: stored.includeProjectInbox !== false,
|
|
101
|
-
includeDocsIntake: stored.includeDocsIntake !== false,
|
|
102
|
-
includeArchetypeInbox: stored.includeArchetypeInbox === true,
|
|
103
|
-
};
|
|
53
|
+
return cfg;
|
|
104
54
|
}
|
|
105
55
|
|
|
106
56
|
export function saveIntakeConfig(rootDir, patch = {}) {
|
|
107
57
|
if (!shouldCreateCx(rootDir)) {
|
|
108
58
|
throw new Error('Refusing to write intake config: directory is not an initialized construct project. Run `construct init` first.');
|
|
109
59
|
}
|
|
110
|
-
const current = loadIntakeConfig(rootDir, {});
|
|
111
|
-
const next = {
|
|
112
|
-
parentDirs: Array.isArray(patch.parentDirs)
|
|
113
|
-
? patch.parentDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
|
|
114
|
-
: current.parentDirs,
|
|
115
|
-
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
116
|
-
includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
|
|
117
|
-
includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
|
|
118
|
-
includeArchetypeInbox: patch.includeArchetypeInbox !== undefined ? Boolean(patch.includeArchetypeInbox) : current.includeArchetypeInbox,
|
|
119
|
-
};
|
|
120
60
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
61
|
+
const policyPatch = {};
|
|
62
|
+
if (patch.maxDepth !== undefined) policyPatch.maxDepth = patch.maxDepth;
|
|
63
|
+
if (patch.includeProjectInbox !== undefined) policyPatch.projectInbox = patch.includeProjectInbox;
|
|
64
|
+
if (patch.includeDocsIntake !== undefined) policyPatch.docsIntake = patch.includeDocsIntake;
|
|
65
|
+
if (patch.includeArchetypeInbox !== undefined || patch.includeRootInbox !== undefined) {
|
|
66
|
+
policyPatch.rootInbox = patch.includeRootInbox ?? patch.includeArchetypeInbox;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(patch.parentDirs)) policyPatch.additionalDirs = patch.parentDirs;
|
|
69
|
+
|
|
70
|
+
const { policy } = saveIntakePolicy(rootDir, policyPatch);
|
|
71
|
+
return policyToLegacyShape(policy);
|
|
125
72
|
}
|
|
126
73
|
|
|
127
|
-
|
|
128
|
-
const value = clampDepth(depth);
|
|
129
|
-
const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
|
|
130
|
-
if (exact) return exact;
|
|
74
|
+
function policyToLegacyShape(policy) {
|
|
131
75
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
76
|
+
parentDirs: policy.additionalDirs,
|
|
77
|
+
maxDepth: policy.maxDepth,
|
|
78
|
+
includeProjectInbox: policy.zones.projectInbox,
|
|
79
|
+
includeDocsIntake: policy.zones.docsIntake,
|
|
80
|
+
includeArchetypeInbox: policy.zones.rootInbox,
|
|
81
|
+
includeRootInbox: policy.zones.rootInbox,
|
|
135
82
|
};
|
|
136
83
|
}
|