@geraldmaron/construct 1.2.0 → 1.2.2
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 +8 -7
- package/bin/construct +206 -62
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +13 -11
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +23 -6
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +139 -0
- package/lib/config/project-config.mjs +106 -7
- package/lib/config/schema.mjs +45 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +39 -17
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +9 -6
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -41
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/export-branding.mjs +32 -0
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +20 -16
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +47 -92
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +32 -112
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +20 -2
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/playwright-demo.mjs +2 -0
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +53 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +5 -3
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +9 -8
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +3 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
package/lib/auto-docs.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* buildSite() is retained for compatibility; the public site now renders docs/
|
|
13
13
|
* directly through Next.js (apps/docs/) and buildFumadocsReference() emits
|
|
14
|
-
* generated reference Markdown into docs/reference/.
|
|
14
|
+
* generated reference Markdown into docs/guides/reference/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import fs from 'node:fs';
|
|
@@ -78,7 +78,7 @@ function buildCoreDocsContract() {
|
|
|
78
78
|
'| `.cx/context.md` | Human-readable resumable project context | Active work, decisions, architecture assumptions, or open questions change |',
|
|
79
79
|
'| `.cx/context.json` | Machine-readable resumable context | Context state needs to stay in sync with `.cx/context.md` |',
|
|
80
80
|
'| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
|
|
81
|
-
'| `docs/concepts/architecture.mdx` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
81
|
+
'| `docs/guides/concepts/architecture.mdx` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
82
82
|
'',
|
|
83
83
|
'`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed; durable work belongs in the tracker (Beads or external).',
|
|
84
84
|
'',
|
|
@@ -191,7 +191,7 @@ function buildAgentsTable(rootDir) {
|
|
|
191
191
|
// --- public API ---
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* Regenerate all AUTO regions in README.md, docs/README.md, and docs/concepts/architecture.mdx.
|
|
194
|
+
* Regenerate all AUTO regions in README.md, docs/README.md, and docs/guides/concepts/architecture.mdx.
|
|
195
195
|
* Returns { changed: string[], checked: boolean }.
|
|
196
196
|
* With check:true writes nothing and sets changed to files that would differ.
|
|
197
197
|
*/
|
|
@@ -209,7 +209,7 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
|
|
|
209
209
|
},
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
file: path.join(rootDir, 'docs', 'concepts', 'architecture.mdx'),
|
|
212
|
+
file: path.join(rootDir, 'docs', 'guides', 'concepts', 'architecture.mdx'),
|
|
213
213
|
regions: {
|
|
214
214
|
agents: buildAgentsTable(rootDir),
|
|
215
215
|
},
|
|
@@ -268,7 +268,7 @@ export function checkDocsCoverage({ rootDir } = {}) {
|
|
|
268
268
|
// Build a combined corpus: docs/README.md + every linked guide file
|
|
269
269
|
let corpus = docsReadme;
|
|
270
270
|
for (const target of linkTargets) {
|
|
271
|
-
if (!target.startsWith('./
|
|
271
|
+
if (!target.startsWith('./guides/cookbook/')) continue;
|
|
272
272
|
const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
|
|
273
273
|
const content = readFile(filePath);
|
|
274
274
|
if (content) corpus += '\n' + content;
|
|
@@ -368,7 +368,9 @@ function renderCommandPage(category, commands) {
|
|
|
368
368
|
lines.push('**Subcommands**');
|
|
369
369
|
lines.push('');
|
|
370
370
|
for (const sub of cmd.subcommands) {
|
|
371
|
-
|
|
371
|
+
const name = typeof sub === 'string' ? sub : sub.name;
|
|
372
|
+
const description = typeof sub === 'string' ? '' : (sub.desc || sub.description || '');
|
|
373
|
+
lines.push(`- \`${name}\`${description ? ` — ${description}` : ''}`);
|
|
372
374
|
}
|
|
373
375
|
lines.push('');
|
|
374
376
|
}
|
|
@@ -421,7 +423,7 @@ function renderAgentsPage(rootDir) {
|
|
|
421
423
|
}
|
|
422
424
|
|
|
423
425
|
/**
|
|
424
|
-
* Emit MDX reference pages into docs/reference/ for the Next.js docs site
|
|
426
|
+
* Emit MDX reference pages into docs/guides/reference/ for the Next.js docs site
|
|
425
427
|
* in apps/docs/. Mirrors the data sources behind AUTO regions and buildSite()
|
|
426
428
|
* but writes Markdown files the docs app can pull in directly (no MkDocs
|
|
427
429
|
* intermediate). Name kept for backwards compatibility with callers that
|
|
@@ -430,7 +432,7 @@ function renderAgentsPage(rootDir) {
|
|
|
430
432
|
*/
|
|
431
433
|
export function buildFumadocsReference({ rootDir } = {}) {
|
|
432
434
|
rootDir = rootDir ?? process.cwd();
|
|
433
|
-
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
435
|
+
const refDir = path.join(rootDir, 'docs', 'guides', 'reference');
|
|
434
436
|
const cliDir = path.join(refDir, 'cli');
|
|
435
437
|
fs.mkdirSync(cliDir, { recursive: true });
|
|
436
438
|
|
|
@@ -466,7 +468,7 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
466
468
|
const slug = slugify(cat);
|
|
467
469
|
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
468
470
|
if (!commands.length) return null;
|
|
469
|
-
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
471
|
+
return `- [${cat}](/guides/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
470
472
|
}).filter(Boolean),
|
|
471
473
|
'',
|
|
472
474
|
].join('\n');
|
|
@@ -497,7 +499,7 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
497
499
|
|
|
498
500
|
export function checkFumadocsReferenceDrift({ rootDir } = {}) {
|
|
499
501
|
rootDir = rootDir ?? process.cwd();
|
|
500
|
-
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
502
|
+
const refDir = path.join(rootDir, 'docs', 'guides', 'reference');
|
|
501
503
|
const cliDir = path.join(refDir, 'cli');
|
|
502
504
|
const drift = [];
|
|
503
505
|
|
|
@@ -532,7 +534,7 @@ export function checkFumadocsReferenceDrift({ rootDir } = {}) {
|
|
|
532
534
|
const slug = slugify(cat);
|
|
533
535
|
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
534
536
|
if (!commands.length) return null;
|
|
535
|
-
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
537
|
+
return `- [${cat}](/guides/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
536
538
|
}).filter(Boolean),
|
|
537
539
|
'',
|
|
538
540
|
].join('\n');
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
-
import os from 'node:os';
|
|
18
17
|
import { registerResource, getResource } from './resources.mjs';
|
|
18
|
+
import { cacheDir } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
20
|
function probeCommandVersion(cmd, args = ['--version']) {
|
|
21
21
|
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 3000 });
|
|
@@ -56,8 +56,8 @@ async function probePostgresContainer() {
|
|
|
56
56
|
|
|
57
57
|
async function probeEmbeddingModel() {
|
|
58
58
|
const cachePath = join(
|
|
59
|
-
|
|
60
|
-
'
|
|
59
|
+
cacheDir(),
|
|
60
|
+
'embeddings',
|
|
61
61
|
'Xenova', 'all-MiniLM-L6-v2', 'onnx', 'model_quantized.onnx'
|
|
62
62
|
);
|
|
63
63
|
if (existsSync(cachePath)) {
|
package/lib/boundary.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Verifies a parent Construct instance is reachable, validates an HMAC
|
|
5
5
|
* signature over `childInstanceId|nonce` when a shared secret is configured,
|
|
6
|
-
* and persists the binding to
|
|
6
|
+
* and persists the binding to boundary.json in the XDG config dir (mode 0600). A
|
|
7
7
|
* different parent rotates the registration only when explicitly allowed via
|
|
8
8
|
* CONSTRUCT_BOUNDARY_ALLOW_OVERRIDE=1; prior configs are archived alongside
|
|
9
9
|
* the active one. Exposed as a standalone module so the dashboard endpoint
|
|
@@ -16,11 +16,12 @@ import { homedir } from 'node:os';
|
|
|
16
16
|
import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto';
|
|
17
17
|
import { request as httpRequest } from 'node:http';
|
|
18
18
|
import { request as httpsRequest } from 'node:https';
|
|
19
|
+
import { configDir } from './config/xdg.mjs';
|
|
19
20
|
|
|
20
21
|
export const BOUNDARY_VERSION = '1.0';
|
|
21
22
|
|
|
22
23
|
function boundaryDir(home = homedir()) {
|
|
23
|
-
return
|
|
24
|
+
return configDir(home);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function boundaryConfigPath(home = homedir()) {
|
package/lib/brand-prose.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const EXCLUDE_PATH =
|
|
|
37
37
|
/(node_modules|\.git|audit-artifacts|scripts\/audit|CHANGELOG\.md|fonts\/legacy\/|\.next\/|\/out\/|tests\/e2e\/reports\/|package-lock\.json)/;
|
|
38
38
|
|
|
39
39
|
const DOC_EXEMPT =
|
|
40
|
-
/(?:^|\/)(docs\/STYLE\.md|docs\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
|
|
40
|
+
/(?:^|\/)(docs\/STYLE\.md|docs\/guides\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
|
|
41
41
|
|
|
42
42
|
const HOOK_SCOPED =
|
|
43
43
|
/^(docs\/|skills\/|specialists\/|personas\/|templates\/|rules\/|apps\/(dashboard|docs|chat)\/)/;
|
package/lib/cache-governor.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
13
|
import { normalizeCostEntry } from './cost.mjs';
|
|
14
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
15
15
|
|
|
16
|
-
const STRATEGY_PATH = join(
|
|
16
|
+
const STRATEGY_PATH = join(doctorRoot(), 'cache-strategy.json');
|
|
17
17
|
const REVIEW_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes between adjustments
|
|
18
18
|
|
|
19
19
|
function readStrategy() {
|
|
@@ -27,7 +27,7 @@ function readStrategy() {
|
|
|
27
27
|
|
|
28
28
|
function writeStrategy(data) {
|
|
29
29
|
try {
|
|
30
|
-
mkdirSync(
|
|
30
|
+
mkdirSync(doctorRoot(), { recursive: true });
|
|
31
31
|
writeFileSync(STRATEGY_PATH, JSON.stringify(data, null, 2));
|
|
32
32
|
} catch { /* best effort */ }
|
|
33
33
|
}
|
|
@@ -153,7 +153,7 @@ export function onPostToolUse({ costLogPath, provider, dryRun = false } = {}) {
|
|
|
153
153
|
// Read cost log
|
|
154
154
|
const entries = [];
|
|
155
155
|
try {
|
|
156
|
-
const logPath = costLogPath || join(
|
|
156
|
+
const logPath = costLogPath || join(doctorRoot(), 'session-cost.jsonl');
|
|
157
157
|
if (!existsSync(logPath)) return { adjusted: false, reason: 'No cost log' };
|
|
158
158
|
|
|
159
159
|
const lines = readFileSync(logPath, 'utf8')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/certification/document-io-fixtures.mjs — intake fixture catalog for document I/O tests.
|
|
3
3
|
*
|
|
4
|
-
* Maps docs/reference/document-io.md supported intake categories to committed
|
|
4
|
+
* Maps docs/guides/reference/document-io.md supported intake categories to committed
|
|
5
5
|
* files under tests/fixtures/document-io/<category>/.
|
|
6
6
|
*/
|
|
7
7
|
|
package/lib/chat/config.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import os from 'node:os';
|
|
18
17
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
18
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
20
|
export const LAYER_KEYS = ['thinking', 'path', 'specialists', 'tools', 'observability'];
|
|
21
21
|
export const PERMISSION_MODES = ['ask', 'allow_once', 'allow_always', 'reject'];
|
|
@@ -64,7 +64,7 @@ function mergeConfig(base, override) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function globalConfigPath() {
|
|
67
|
-
return path.join(
|
|
67
|
+
return path.join(doctorRoot(), CONFIG_BASENAME);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function projectConfigPath({ cwd = process.cwd() } = {}) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/chat/desktop-binary.mjs — resolve the Construct chat desktop window binary.
|
|
3
3
|
*
|
|
4
|
-
* Search order: CONSTRUCT_CHAT_DESKTOP_BIN, repo dev build,
|
|
5
|
-
* then packaged sidecar next to construct.
|
|
4
|
+
* Search order: CONSTRUCT_CHAT_DESKTOP_BIN, repo dev build, the bin/ subdir of
|
|
5
|
+
* the XDG state dir, then packaged sidecar next to construct.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
@@ -10,6 +10,8 @@ import os from 'node:os';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
14
|
+
|
|
13
15
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
15
17
|
|
|
@@ -50,7 +52,7 @@ export function desktopBinaryCandidates({ repoRoot = REPO_ROOT, homeDir = os.hom
|
|
|
50
52
|
env.CONSTRUCT_CHAT_DESKTOP_BIN,
|
|
51
53
|
path.join(tauriRoot, 'release', name),
|
|
52
54
|
path.join(tauriRoot, 'debug', name),
|
|
53
|
-
path.join(homeDir, '
|
|
55
|
+
path.join(stateDir(homeDir), 'bin', name),
|
|
54
56
|
path.join(repoRoot, 'apps', 'chat', 'desktop', 'bin', name),
|
|
55
57
|
].filter(Boolean);
|
|
56
58
|
}
|
package/lib/chat/present.mjs
CHANGED
|
@@ -225,6 +225,7 @@ export function sessionMetaToSsePayload({ session, layers, workingBranch, oracle
|
|
|
225
225
|
return {
|
|
226
226
|
type: 'session_meta',
|
|
227
227
|
model: session?.model || null,
|
|
228
|
+
demoLabel: session?.demoGuide ? (session.demoTitle || 'scripted') : null,
|
|
228
229
|
modelMode: session?.modelMode || 'pinned',
|
|
229
230
|
sandbox: session?.sandbox || null,
|
|
230
231
|
permissionMode: session?.permissionMode || null,
|
package/lib/cli-commands.mjs
CHANGED
|
@@ -330,12 +330,14 @@ export const CLI_COMMANDS = [
|
|
|
330
330
|
category: 'Work',
|
|
331
331
|
core: false,
|
|
332
332
|
description: 'Export markdown to PDF, DOCX, HTML, and other Pandoc formats via Pandoc + Typst (optional system binaries; ADR-0024)',
|
|
333
|
-
usage: 'construct export <markdown-file> --to=<pdf|docx|deck|pptx|html|rtf|odt|epub|tex|txt|md|mdx> [--output=<path>] [--figures] [--detect]',
|
|
333
|
+
usage: 'construct export <markdown-file> --to=<pdf|docx|deck|pptx|html|rtf|odt|epub|tex|txt|md|mdx> [--output=<path>] [--figures|--no-figures] [--plain] [--detect]',
|
|
334
334
|
strictFlags: true,
|
|
335
335
|
options: [
|
|
336
336
|
{ flag: '--to=<format>', desc: 'pdf, docx, deck, pptx, html, rtf, odt, epub, tex, txt, md, mdx' },
|
|
337
337
|
{ flag: '--output=<path>', desc: 'Output path' },
|
|
338
338
|
{ flag: '--figures', desc: 'Render d2/mermaid via pandoc-ext/diagram filter' },
|
|
339
|
+
{ flag: '--no-figures', desc: 'Skip diagram rendering' },
|
|
340
|
+
{ flag: '--plain, --no-brand', desc: 'Explicitly opt out of Construct branding for a brand-capable output' },
|
|
339
341
|
{ flag: '--detect', desc: 'Report binary availability (JSON)' },
|
|
340
342
|
],
|
|
341
343
|
},
|
|
@@ -658,10 +660,11 @@ export const CLI_COMMANDS = [
|
|
|
658
660
|
emoji: '📋',
|
|
659
661
|
category: 'Work',
|
|
660
662
|
core: false,
|
|
661
|
-
description: '
|
|
662
|
-
usage: 'construct artifact validate
|
|
663
|
+
description: 'Plan or locally execute manifest-backed artifact workflows with execution provenance',
|
|
664
|
+
usage: 'construct artifact <validate|workflow> ...',
|
|
663
665
|
subcommands: [
|
|
664
666
|
{ name: 'validate', desc: 'Run manifest structure, citation, and reviewer checks' },
|
|
667
|
+
{ name: 'workflow', desc: 'Return a truthful plan/run report; --apply only runs local validation/export after approval' },
|
|
665
668
|
],
|
|
666
669
|
},
|
|
667
670
|
{
|
|
@@ -772,6 +775,20 @@ export const CLI_COMMANDS = [
|
|
|
772
775
|
description: 'Deployment mode configuration',
|
|
773
776
|
usage: 'construct config <get|set>',
|
|
774
777
|
},
|
|
778
|
+
{
|
|
779
|
+
name: 'sources',
|
|
780
|
+
emoji: '🔗',
|
|
781
|
+
category: 'Advanced',
|
|
782
|
+
core: false,
|
|
783
|
+
description: 'Manage typed integration source targets in construct.config.json',
|
|
784
|
+
usage: 'construct sources list|add|remove|validate',
|
|
785
|
+
subcommands: [
|
|
786
|
+
{ name: 'list', desc: 'Show config targets, legacy env merge, and effective set' },
|
|
787
|
+
{ name: 'add <provider> <id> <selector-json>', desc: 'Add a typed target (github, jira, linear, slack)' },
|
|
788
|
+
{ name: 'remove <id>', desc: 'Remove a config target by id' },
|
|
789
|
+
{ name: 'validate', desc: 'Validate sources.targets in construct.config.json' },
|
|
790
|
+
],
|
|
791
|
+
},
|
|
775
792
|
{
|
|
776
793
|
name: 'uninstall',
|
|
777
794
|
emoji: '🧹',
|
|
@@ -1095,7 +1112,7 @@ export const CLI_COMMANDS = [
|
|
|
1095
1112
|
emoji: '📄',
|
|
1096
1113
|
category: 'Diagnostics',
|
|
1097
1114
|
core: false,
|
|
1098
|
-
description: 'Regenerate generated reference pages under docs/reference/',
|
|
1115
|
+
description: 'Regenerate generated reference pages under docs/guides/reference/',
|
|
1099
1116
|
usage: 'construct docs:site [--check]',
|
|
1100
1117
|
},
|
|
1101
1118
|
{
|
|
@@ -1129,7 +1146,7 @@ export const CLI_COMMANDS = [
|
|
|
1129
1146
|
{ name: 'specialist', category: 'Internal', core: false, internal: true, description: 'Maintainer tool: scaffold, edit, and lint specialist prompts', usage: 'construct specialist <create|edit|lint>' },
|
|
1130
1147
|
{ name: 'registry:status', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Dev: capability registry inspector', usage: 'construct registry:status [--json]' },
|
|
1131
1148
|
{ name: 'registry:validate', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Validate registry/capabilities.json against repo reality', usage: 'construct registry:validate [--json]' },
|
|
1132
|
-
{ name: 'registry:generate-docs', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Generate docs/reference/capabilities.md from registry', usage: 'construct registry:generate-docs' },
|
|
1149
|
+
{ name: 'registry:generate-docs', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Generate docs/guides/reference/capabilities.md from registry', usage: 'construct registry:generate-docs' },
|
|
1133
1150
|
{ name: 'oracle', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Oracle meta-controller — fleet health review and bounded-auto maintenance', usage: 'construct oracle start|status|review|pending|approve|gaps|reconcile' },
|
|
1134
1151
|
{ name: 'matrix', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Living dependency matrix — build/inspect the typed file↔capability↔workflow↔test graph', usage: 'construct matrix build|stat|query <node-id> [--json]' },
|
|
1135
1152
|
{ name: 'impact', category: 'Diagnostics', core: false, internal: false, surface: 'thin-cli', description: 'Change-impact analysis — map changed files to affected tests, capabilities, and workflows', usage: 'construct impact [files…] [--stdin] [--run] [--json]' },
|
|
@@ -1254,7 +1271,7 @@ export function formatCommandHelp(name, { colors = false } = {}) {
|
|
|
1254
1271
|
lines.push(`${c.dim}Next:${c.reset} ${next}`);
|
|
1255
1272
|
lines.push('');
|
|
1256
1273
|
}
|
|
1257
|
-
lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/reference/cli/.`);
|
|
1274
|
+
lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/guides/reference/cli/.`);
|
|
1258
1275
|
lines.push('');
|
|
1259
1276
|
return lines.join('\n');
|
|
1260
1277
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cli-service-inventory.mjs — auditable public CLI/catalog/docs inventory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CLI_COMMANDS } from './cli-commands.mjs';
|
|
8
|
+
|
|
9
|
+
function slugify(category) {
|
|
10
|
+
return category.toLowerCase().replace(/&/g, 'and').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function handlerNames(rootDir) {
|
|
14
|
+
const source = fs.readFileSync(path.join(rootDir, 'bin', 'construct'), 'utf8');
|
|
15
|
+
const names = new Set();
|
|
16
|
+
for (const match of source.matchAll(/\n {2,3}\[\s*'([^']+)'\s*,/g)) names.add(match[1]);
|
|
17
|
+
return names;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The catalog is the public-service source of truth. This projection joins it
|
|
22
|
+
* to runtime dispatch and the generated reference pages so drift is observable
|
|
23
|
+
* without treating internal handlers as public services.
|
|
24
|
+
*/
|
|
25
|
+
export function buildCliServiceInventory({ rootDir = process.cwd() } = {}) {
|
|
26
|
+
const handlers = handlerNames(rootDir);
|
|
27
|
+
return CLI_COMMANDS.filter((spec) => !spec.internal).map((spec) => {
|
|
28
|
+
const page = path.join(rootDir, 'docs', 'guides', 'reference', 'cli', `${slugify(spec.category)}.md`);
|
|
29
|
+
const pageText = fs.existsSync(page) ? fs.readFileSync(page, 'utf8') : '';
|
|
30
|
+
return {
|
|
31
|
+
name: spec.name,
|
|
32
|
+
category: spec.category,
|
|
33
|
+
runnable: handlers.has(spec.name),
|
|
34
|
+
documented: pageText.includes(`## construct ${spec.name}`),
|
|
35
|
+
usage: spec.usage,
|
|
36
|
+
subcommands: (spec.subcommands ?? []).map((sub) => ({
|
|
37
|
+
name: typeof sub === 'string' ? sub : sub.name,
|
|
38
|
+
documented: typeof sub === 'string' ? true : Boolean(sub.desc || sub.description),
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -34,11 +34,15 @@ const MD_HEADER_GLOBS = [
|
|
|
34
34
|
// Scope is deliberately narrow — READMEs, cookbook, concepts, and reference are
|
|
35
35
|
// unaffected. The patterns live in BANNED_ARTIFACT below; the severity (block
|
|
36
36
|
// vs warn) is controlled by CONSTRUCT_ARTIFACT_LINT_MODE.
|
|
37
|
+
// Match this repo's bucketed layout (docs/specs/prd, docs/decisions/adr, …) and
|
|
38
|
+
// the flat init-lane layout that `construct init` scaffolds downstream (docs/prd,
|
|
39
|
+
// docs/adr, …), so artifact-prose linting fires in either project shape.
|
|
40
|
+
|
|
37
41
|
const ARTIFACT_PATH_GLOBS = [
|
|
38
|
-
/^docs\/prd\//,
|
|
39
|
-
/^docs\/adr\//,
|
|
40
|
-
/^docs\/rfc\//,
|
|
41
|
-
/^docs\/research\//,
|
|
42
|
+
/^docs\/(?:specs\/)?prd\//,
|
|
43
|
+
/^docs\/(?:decisions\/)?adr\//,
|
|
44
|
+
/^docs\/(?:decisions\/)?rfc\//,
|
|
45
|
+
/^docs\/(?:notes\/)?research\//,
|
|
42
46
|
/^\.cx\/knowledge\//,
|
|
43
47
|
/^\.cx\/handoffs\//,
|
|
44
48
|
/^\.cx\/research\//,
|
|
@@ -55,7 +59,7 @@ function isArtifactPath(rel) {
|
|
|
55
59
|
// Deliverable surfaces where a tool-identity leak (rules/common/tool-invisibility.md)
|
|
56
60
|
// matters: project docs and the durable .cx knowledge / research / handoff / strategy
|
|
57
61
|
// stores. Broader than the fabrication-scoped ARTIFACT_PATH_GLOBS because a strategy
|
|
58
|
-
// lands in docs/ root, not only docs/prd.
|
|
62
|
+
// lands in docs/ root, not only docs/specs/prd.
|
|
59
63
|
|
|
60
64
|
const DELIVERABLE_LEAK_GLOBS = [
|
|
61
65
|
/^docs\/.*\.md$/,
|
package/lib/config/alias.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lookup precedence:
|
|
5
5
|
* 1. CONSTRUCT_ALIAS env var (per-session override)
|
|
6
|
-
* 2.
|
|
6
|
+
* 2. config.json.aliasOverride in the XDG config dir (user-level, lets each
|
|
7
7
|
* user on the same project see their own branding without
|
|
8
8
|
* committing it)
|
|
9
9
|
* 3. construct.config.json.alias (project-level, committed)
|
|
@@ -17,6 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { loadProjectConfig } from './project-config.mjs';
|
|
20
|
+
import { configDir } from './xdg.mjs';
|
|
20
21
|
|
|
21
22
|
export const DEFAULT_ALIAS = 'Construct';
|
|
22
23
|
export const ALIAS_ENV_KEY = 'CONSTRUCT_ALIAS';
|
|
@@ -29,7 +30,7 @@ export function resolveAlias({ cwd = process.cwd(), env = process.env, homeDir =
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
|
-
const userCfgPath = path.join(homeDir, '
|
|
33
|
+
const userCfgPath = path.join(configDir(homeDir), 'config.json');
|
|
33
34
|
if (fs.existsSync(userCfgPath)) {
|
|
34
35
|
const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8'));
|
|
35
36
|
const userAlias = userCfg?.aliasOverride;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/intake-policy.mjs — project-scoped intake watcher policy.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the canonical drop zone, scan depth, and additional directories from
|
|
5
|
+
* construct.config.json intakePolicy (the only config source), with env
|
|
6
|
+
* overrides and a default when intakePolicy is absent.
|
|
7
|
+
*
|
|
8
|
+
* Single-zone model (ADR-0045 §C): the only drop zone is the project-root
|
|
9
|
+
* `inbox/`, always watched. `additionalDirs` (opt-in extra dirs) and `maxDepth`
|
|
10
|
+
* remain configurable. There are no other zones — `.cx/inbox/` and `docs/intake/`
|
|
11
|
+
* are not watched and not scaffolded.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
15
|
+
import { loadProjectConfig, writeProjectConfig, findProjectConfigPath, PROJECT_CONFIG_FILENAME } from './project-config.mjs';
|
|
16
|
+
import {
|
|
17
|
+
INTAKE_DEFAULT_MAX_DEPTH,
|
|
18
|
+
INTAKE_HARD_MAX_DEPTH,
|
|
19
|
+
INTAKE_DEPTH_GUIDANCE,
|
|
20
|
+
describeIntakeDepth,
|
|
21
|
+
} from '../intake/constants.mjs';
|
|
22
|
+
|
|
23
|
+
export { INTAKE_DEFAULT_MAX_DEPTH, INTAKE_HARD_MAX_DEPTH, INTAKE_DEPTH_GUIDANCE, describeIntakeDepth };
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_INTAKE_POLICY = Object.freeze({
|
|
26
|
+
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
27
|
+
additionalDirs: [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function clampDepth(value) {
|
|
31
|
+
const n = Number(value);
|
|
32
|
+
if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
|
|
33
|
+
return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeDir(dir, rootDir) {
|
|
37
|
+
if (typeof dir !== 'string') return null;
|
|
38
|
+
const trimmed = dir.trim();
|
|
39
|
+
if (!trimmed) return null;
|
|
40
|
+
return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseEnvDirs(env) {
|
|
44
|
+
const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
|
|
45
|
+
if (!raw) return [];
|
|
46
|
+
return raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function intakePolicyFromProjectConfig(config) {
|
|
50
|
+
const raw = config?.intakePolicy ?? {};
|
|
51
|
+
return {
|
|
52
|
+
maxDepth: clampDepth(raw.maxDepth ?? DEFAULT_INTAKE_POLICY.maxDepth),
|
|
53
|
+
additionalDirs: Array.isArray(raw.additionalDirs)
|
|
54
|
+
? raw.additionalDirs.map((d) => String(d).trim()).filter(Boolean)
|
|
55
|
+
: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function loadIntakePolicy(rootDir, env = process.env) {
|
|
60
|
+
const { config, path: configPath, raw } = loadProjectConfig(rootDir, env);
|
|
61
|
+
let policy;
|
|
62
|
+
let source = 'default';
|
|
63
|
+
|
|
64
|
+
if (configPath && raw?.intakePolicy) {
|
|
65
|
+
policy = intakePolicyFromProjectConfig(config);
|
|
66
|
+
source = 'project-config';
|
|
67
|
+
} else {
|
|
68
|
+
policy = structuredClone(DEFAULT_INTAKE_POLICY);
|
|
69
|
+
source = 'default';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const envDirs = parseEnvDirs(env).map((dir) => normalizeDir(dir, rootDir)).filter(Boolean);
|
|
73
|
+
const additionalDirs = []
|
|
74
|
+
.concat(policy.additionalDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean))
|
|
75
|
+
.concat(envDirs);
|
|
76
|
+
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const uniqueDirs = [];
|
|
79
|
+
for (const dir of additionalDirs) {
|
|
80
|
+
if (seen.has(dir)) continue;
|
|
81
|
+
seen.add(dir);
|
|
82
|
+
uniqueDirs.push(dir);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const envDepth = env?.CX_INTAKE_MAX_DEPTH;
|
|
86
|
+
const maxDepth = clampDepth(envDepth ?? policy.maxDepth);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
maxDepth,
|
|
90
|
+
additionalDirs: uniqueDirs,
|
|
91
|
+
source,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolvedIntakeConfig(rootDir, env = process.env) {
|
|
96
|
+
const policy = loadIntakePolicy(rootDir, env);
|
|
97
|
+
return {
|
|
98
|
+
parentDirs: policy.additionalDirs,
|
|
99
|
+
maxDepth: policy.maxDepth,
|
|
100
|
+
source: policy.source,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function saveIntakePolicy(rootDir, patch = {}, options = {}) {
|
|
105
|
+
const configPath = findProjectConfigPath(rootDir) ?? join(rootDir, PROJECT_CONFIG_FILENAME);
|
|
106
|
+
const { config } = loadProjectConfig(rootDir, {});
|
|
107
|
+
const current = intakePolicyFromProjectConfig(config.intakePolicy ? config : { intakePolicy: DEFAULT_INTAKE_POLICY });
|
|
108
|
+
|
|
109
|
+
const next = {
|
|
110
|
+
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
111
|
+
additionalDirs: Array.isArray(patch.additionalDirs)
|
|
112
|
+
? patch.additionalDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
|
|
113
|
+
: current.additionalDirs,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (patch.addDir) {
|
|
117
|
+
const dir = normalizeDir(patch.addDir, rootDir);
|
|
118
|
+
if (dir && !next.additionalDirs.includes(dir)) {
|
|
119
|
+
next.additionalDirs = [...next.additionalDirs, dir];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (patch.removeDir) {
|
|
123
|
+
const dir = normalizeDir(patch.removeDir, rootDir);
|
|
124
|
+
next.additionalDirs = next.additionalDirs.filter((d) => d !== dir);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const updated = {
|
|
128
|
+
...config,
|
|
129
|
+
version: config.version ?? 1,
|
|
130
|
+
intakePolicy: next,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (options.dryRun) {
|
|
134
|
+
return { policy: next, configPath, dryRun: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeProjectConfig(configPath, updated);
|
|
138
|
+
return { policy: next, configPath };
|
|
139
|
+
}
|