@geraldmaron/construct 1.2.1 → 1.2.3
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 +6 -6
- package/apps/chat/engine/ai-sdk-agent.mjs +183 -0
- package/apps/chat/engine/loop-driver.mjs +211 -0
- package/apps/chat/engine/models.mjs +122 -0
- package/apps/chat/engine/provider-adapters.mjs +171 -0
- package/apps/chat/engine/tools/permission.mjs +54 -0
- package/apps/chat/engine/tools/primitives.mjs +180 -0
- package/apps/chat/engine/tools/registry.mjs +122 -0
- package/apps/chat/engine/turn-controls.mjs +70 -0
- package/bin/construct +93 -79
- 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-type-from-path.mjs +9 -5
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +10 -10
- 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/cli-commands.mjs +4 -6
- package/lib/cli-service-inventory.mjs +1 -1
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +8 -78
- package/lib/config/legacy-config-migration.mjs +59 -0
- package/lib/config/project-config.mjs +95 -5
- package/lib/config/schema.mjs +9 -19
- 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/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 +1 -1
- 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/cli.mjs +7 -5
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -37
- 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/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- 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 +6 -5
- 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 +35 -81
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +5 -32
- 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 +1 -1
- 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/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-env.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 +24 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/webhook.mjs +2 -1
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +21 -11
- 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 +4 -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/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/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/intake/legacy-paths.mjs +0 -5
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
* corresponding env-var value at load time. Pointers keep API keys in
|
|
9
9
|
* `.env` where they belong; the JSON config never sees a secret literal.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
11
|
+
* Two project tiers, most-specific wins: a gitignored
|
|
12
|
+
* `construct.config.local.json` sibling merges OVER the committed
|
|
13
|
+
* `construct.config.json`. List settings (sources.targets, intake
|
|
14
|
+
* additionalDirs, permission allowlists) merge + dedupe; scalars and objects
|
|
15
|
+
* override key-by-key.
|
|
16
|
+
*
|
|
17
|
+
* Precedence rule for consumers: env var if set > config (local ▷ committed)
|
|
18
|
+
* > default. `resolveSetting(config, jsonPath, env, envKey, default)` encodes
|
|
19
|
+
* the rule once so call sites stay consistent.
|
|
14
20
|
*/
|
|
15
21
|
import fs from 'node:fs';
|
|
16
22
|
import path from 'node:path';
|
|
@@ -18,6 +24,7 @@ import { CONFIG_SCHEMA_VERSION, DEFAULT_PROJECT_CONFIG, validateProjectConfig, F
|
|
|
18
24
|
import { validateSourceTargets } from './source-targets.mjs';
|
|
19
25
|
|
|
20
26
|
export const PROJECT_CONFIG_FILENAME = 'construct.config.json';
|
|
27
|
+
export const PROJECT_LOCAL_CONFIG_FILENAME = 'construct.config.local.json';
|
|
21
28
|
|
|
22
29
|
export function findProjectConfigPath(cwd = process.cwd()) {
|
|
23
30
|
let dir = path.resolve(cwd);
|
|
@@ -62,6 +69,64 @@ function deepMerge(base, override) {
|
|
|
62
69
|
return out;
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
// Identity for list-tier dedupe: object entries collapse on their `id`
|
|
73
|
+
// (source targets, typed selectors) so a local override that re-declares an id
|
|
74
|
+
// replaces the committed entry rather than duplicating it; everything else
|
|
75
|
+
// dedupes on structural equality.
|
|
76
|
+
|
|
77
|
+
function listKey(item) {
|
|
78
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string') {
|
|
79
|
+
return `id:${item.id}`;
|
|
80
|
+
}
|
|
81
|
+
return `json:${JSON.stringify(item)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mergeList(base, override) {
|
|
85
|
+
const out = [];
|
|
86
|
+
const seen = new Map();
|
|
87
|
+
for (const item of [...(base || []), ...(override || [])]) {
|
|
88
|
+
const key = listKey(item);
|
|
89
|
+
if (seen.has(key)) {
|
|
90
|
+
out[seen.get(key)] = item;
|
|
91
|
+
} else {
|
|
92
|
+
seen.set(key, out.length);
|
|
93
|
+
out.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Project-local tier merge (most-specific wins): lists merge + dedupe so a local
|
|
100
|
+
// override adds to committed lists (extra source targets, intake dirs, allowlist
|
|
101
|
+
// entries) instead of clobbering them; scalars and objects override key-by-key.
|
|
102
|
+
|
|
103
|
+
function mergeConfigTier(base, override) {
|
|
104
|
+
if (override === undefined) return base;
|
|
105
|
+
if (Array.isArray(base) && Array.isArray(override)) return mergeList(base, override);
|
|
106
|
+
if (override === null || typeof override !== 'object' || Array.isArray(override)) return override;
|
|
107
|
+
const out = Array.isArray(base) ? [...(base || [])] : { ...(base || {}) };
|
|
108
|
+
for (const [k, v] of Object.entries(override)) {
|
|
109
|
+
out[k] = mergeConfigTier(base?.[k], v);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadLocalConfigOverlay(configPath) {
|
|
115
|
+
const localPath = path.join(path.dirname(configPath), PROJECT_LOCAL_CONFIG_FILENAME);
|
|
116
|
+
if (!fs.existsSync(localPath)) return { raw: null, path: null, errors: [] };
|
|
117
|
+
let raw;
|
|
118
|
+
try {
|
|
119
|
+
raw = JSON.parse(fs.readFileSync(localPath, 'utf8'));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { raw: null, path: localPath, errors: [`failed to parse ${localPath}: ${err.message}`] };
|
|
122
|
+
}
|
|
123
|
+
const validation = validateProjectConfig(raw, { partial: true });
|
|
124
|
+
if (!validation.valid) {
|
|
125
|
+
return { raw: null, path: localPath, errors: (validation.errors ?? []).map((e) => `${localPath}: ${e}`) };
|
|
126
|
+
}
|
|
127
|
+
return { raw, path: localPath, errors: [] };
|
|
128
|
+
}
|
|
129
|
+
|
|
65
130
|
export function loadProjectConfig(cwd = process.cwd(), env = process.env) {
|
|
66
131
|
const configPath = findProjectConfigPath(cwd);
|
|
67
132
|
if (!configPath) {
|
|
@@ -99,13 +164,38 @@ export function loadProjectConfig(cwd = process.cwd(), env = process.env) {
|
|
|
99
164
|
errors: allErrors,
|
|
100
165
|
};
|
|
101
166
|
}
|
|
167
|
+
const overlay = loadLocalConfigOverlay(configPath);
|
|
168
|
+
if (overlay.errors.length) {
|
|
169
|
+
return {
|
|
170
|
+
path: configPath,
|
|
171
|
+
raw,
|
|
172
|
+
config: structuredClone(DEFAULT_PROJECT_CONFIG),
|
|
173
|
+
source: 'invalid',
|
|
174
|
+
errors: overlay.errors,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (overlay.raw?.sources?.targets !== undefined) {
|
|
178
|
+
const overlayTargetErrors = validateSourceTargets(overlay.raw.sources.targets);
|
|
179
|
+
if (overlayTargetErrors.length) {
|
|
180
|
+
return {
|
|
181
|
+
path: configPath,
|
|
182
|
+
raw,
|
|
183
|
+
config: structuredClone(DEFAULT_PROJECT_CONFIG),
|
|
184
|
+
source: 'invalid',
|
|
185
|
+
errors: overlayTargetErrors.map((e) => `${overlay.path}: ${e}`),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
102
190
|
const merged = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), raw);
|
|
103
|
-
const
|
|
191
|
+
const withLocal = overlay.raw ? mergeConfigTier(merged, overlay.raw) : merged;
|
|
192
|
+
const resolved = interpolateSecrets(withLocal, env);
|
|
104
193
|
return {
|
|
105
194
|
path: configPath,
|
|
106
195
|
raw,
|
|
196
|
+
localPath: overlay.path,
|
|
107
197
|
config: resolved,
|
|
108
|
-
source: 'file',
|
|
198
|
+
source: overlay.raw ? 'file+local' : 'file',
|
|
109
199
|
errors: [],
|
|
110
200
|
};
|
|
111
201
|
}
|
package/lib/config/schema.mjs
CHANGED
|
@@ -53,11 +53,6 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
|
53
53
|
}),
|
|
54
54
|
intakePolicy: Object.freeze({
|
|
55
55
|
maxDepth: 4,
|
|
56
|
-
zones: Object.freeze({
|
|
57
|
-
rootInbox: true,
|
|
58
|
-
projectInbox: true,
|
|
59
|
-
docsIntake: true,
|
|
60
|
-
}),
|
|
61
56
|
additionalDirs: [],
|
|
62
57
|
}),
|
|
63
58
|
artifactWorkflow: Object.freeze({
|
|
@@ -151,15 +146,6 @@ export const FIELD_RULES = {
|
|
|
151
146
|
fields: {
|
|
152
147
|
maxDepth: { type: 'number' },
|
|
153
148
|
additionalDirs: { type: 'array' },
|
|
154
|
-
zones: {
|
|
155
|
-
type: 'object',
|
|
156
|
-
required: false,
|
|
157
|
-
fields: {
|
|
158
|
-
rootInbox: { type: 'boolean' },
|
|
159
|
-
projectInbox: { type: 'boolean' },
|
|
160
|
-
docsIntake: { type: 'boolean' },
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
149
|
},
|
|
164
150
|
},
|
|
165
151
|
artifactWorkflow: {
|
|
@@ -269,10 +255,10 @@ function checkType(value, expected) {
|
|
|
269
255
|
return typeof value === expected;
|
|
270
256
|
}
|
|
271
257
|
|
|
272
|
-
function validateField(value, rule, path) {
|
|
258
|
+
function validateField(value, rule, path, partial = false) {
|
|
273
259
|
const errors = [];
|
|
274
260
|
if (value === undefined) {
|
|
275
|
-
if (rule.required) errors.push(`${path}: required field missing`);
|
|
261
|
+
if (rule.required && !partial) errors.push(`${path}: required field missing`);
|
|
276
262
|
return errors;
|
|
277
263
|
}
|
|
278
264
|
if (!checkType(value, rule.type)) {
|
|
@@ -287,19 +273,23 @@ function validateField(value, rule, path) {
|
|
|
287
273
|
}
|
|
288
274
|
if (rule.fields && checkType(value, 'object')) {
|
|
289
275
|
for (const [key, subRule] of Object.entries(rule.fields)) {
|
|
290
|
-
errors.push(...validateField(value[key], subRule, `${path}.${key}
|
|
276
|
+
errors.push(...validateField(value[key], subRule, `${path}.${key}`, partial));
|
|
291
277
|
}
|
|
292
278
|
}
|
|
293
279
|
return errors;
|
|
294
280
|
}
|
|
295
281
|
|
|
296
|
-
|
|
282
|
+
// `partial` validation skips the required-field check so a sparse override file
|
|
283
|
+
// (construct.config.local.json) validates, while still type- and enum-checking
|
|
284
|
+
// every key that is present.
|
|
285
|
+
|
|
286
|
+
export function validateProjectConfig(raw, { partial = false } = {}) {
|
|
297
287
|
const errors = [];
|
|
298
288
|
if (!checkType(raw, 'object')) {
|
|
299
289
|
return { valid: false, errors: ['root: must be an object'] };
|
|
300
290
|
}
|
|
301
291
|
for (const [key, rule] of Object.entries(FIELD_RULES)) {
|
|
302
|
-
errors.push(...validateField(raw[key], rule, key));
|
|
292
|
+
errors.push(...validateField(raw[key], rule, key, partial));
|
|
303
293
|
}
|
|
304
294
|
if (raw.version !== undefined && raw.version !== CONFIG_SCHEMA_VERSION) {
|
|
305
295
|
errors.push(`version: expected ${CONFIG_SCHEMA_VERSION}, got ${raw.version}`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/xdg.mjs — single source of truth for Construct's user-scope base
|
|
3
|
+
* directories, following the XDG Base Directory specification.
|
|
4
|
+
*
|
|
5
|
+
* Keeps config (versionable settings), state (durable runtime data), and cache
|
|
6
|
+
* (regenerable transients) on separate roots so each can be backed up, synced,
|
|
7
|
+
* and pruned independently:
|
|
8
|
+
*
|
|
9
|
+
* configDir() → $XDG_CONFIG_HOME/construct (default ~/.config/construct)
|
|
10
|
+
* stateDir() → $XDG_STATE_HOME/construct (default ~/.local/state/construct)
|
|
11
|
+
* cacheDir() → $XDG_CACHE_HOME/construct (default ~/.cache/construct)
|
|
12
|
+
*
|
|
13
|
+
* Per the XDG spec, an env var is honored only when it is set AND absolute;
|
|
14
|
+
* a relative or empty value is ignored and the default applies. Every helper
|
|
15
|
+
* takes an optional homeDir for testability, mirroring the existing homeDir
|
|
16
|
+
* params across the codebase.
|
|
17
|
+
*
|
|
18
|
+
* Clean break: there is no legacy `~/.construct/*` read here. Existing installs
|
|
19
|
+
* re-run `construct install --scope=user` to repopulate at the XDG paths.
|
|
20
|
+
*/
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
// Leaf segment appended under each resolved base.
|
|
25
|
+
|
|
26
|
+
const APP = 'construct';
|
|
27
|
+
|
|
28
|
+
// XDG honors an env override only when it names an absolute path; anything
|
|
29
|
+
// relative or empty falls through to the spec default so a stray cwd-relative
|
|
30
|
+
// value can never relocate user state under the process working directory.
|
|
31
|
+
|
|
32
|
+
function resolveBase(envValue, fallbackSegments, homeDir) {
|
|
33
|
+
if (typeof envValue === 'string' && path.isAbsolute(envValue)) {
|
|
34
|
+
return envValue;
|
|
35
|
+
}
|
|
36
|
+
return path.join(homeDir, ...fallbackSegments);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function configDir(homeDir = os.homedir(), env = process.env) {
|
|
40
|
+
const base = resolveBase(env.XDG_CONFIG_HOME, ['.config'], homeDir);
|
|
41
|
+
return path.join(base, APP);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stateDir(homeDir = os.homedir(), env = process.env) {
|
|
45
|
+
const base = resolveBase(env.XDG_STATE_HOME, ['.local', 'state'], homeDir);
|
|
46
|
+
return path.join(base, APP);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The global doctor/telemetry/runtime axis (formerly rooted at ~/.cx) now
|
|
50
|
+
// resolves to the XDG state dir. CONSTRUCT_DOCTOR_ROOT, shared by the
|
|
51
|
+
// telemetry/doctor/oracle ecosystem for test isolation, still wins when set to
|
|
52
|
+
// a non-empty value; otherwise stateDir() supplies the durable default.
|
|
53
|
+
|
|
54
|
+
export function doctorRoot(homeDir = os.homedir(), env = process.env) {
|
|
55
|
+
const override = env.CONSTRUCT_DOCTOR_ROOT;
|
|
56
|
+
if (typeof override === 'string' && override.trim()) return override;
|
|
57
|
+
return stateDir(homeDir, env);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function cacheDir(homeDir = os.homedir(), env = process.env) {
|
|
61
|
+
const base = resolveBase(env.XDG_CACHE_HOME, ['.cache'], homeDir);
|
|
62
|
+
return path.join(base, APP);
|
|
63
|
+
}
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { join, dirname } from 'node:path';
|
|
28
|
-
import { homedir } from 'node:os';
|
|
29
28
|
import { createHash } from 'node:crypto';
|
|
30
29
|
import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
|
|
31
30
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
31
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
32
32
|
|
|
33
|
-
const CX_DIR =
|
|
33
|
+
const CX_DIR = doctorRoot();
|
|
34
34
|
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
35
35
|
|
|
36
36
|
// contract-violations are PROJECT-SCOPED: resolves to
|
package/lib/cost-ledger.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* before they blow the daily budget. Separate from lib/roles/ because both
|
|
6
6
|
* the gateway and the cost watcher consume it.
|
|
7
7
|
*
|
|
8
|
-
* Storage:
|
|
8
|
+
* Storage: <doctorRoot>/cost-ledger.json — small JSON object keyed by `<dayKey>:<personaId>`.
|
|
9
9
|
* Rotates: entries older than 30 days are pruned on each write.
|
|
10
10
|
*
|
|
11
11
|
* Budgets are env-driven defaults; explicit `CONSTRUCT_BUDGET_<PERSONA>` wins.
|
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
|
-
import { homedir } from 'node:os';
|
|
19
18
|
import { join } from 'node:path';
|
|
20
19
|
import { cwd as procCwd } from 'node:process';
|
|
21
20
|
|
|
21
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
22
|
+
|
|
22
23
|
const PRUNE_DAYS = 30;
|
|
23
24
|
const DEFAULT_PER_PERSONA = 10.0;
|
|
24
25
|
const DEFAULT_TOTAL = 50.0;
|
|
25
26
|
|
|
26
27
|
function rootDir() {
|
|
27
|
-
return
|
|
28
|
+
return doctorRoot();
|
|
28
29
|
}
|
|
29
30
|
function ledgerPath() {
|
|
30
31
|
return join(rootDir(), 'cost-ledger.json');
|
package/lib/cost.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
11
12
|
|
|
12
13
|
function numberFrom(...values) {
|
|
13
14
|
for (const value of values) {
|
|
@@ -82,7 +83,7 @@ export function normalizeCostEntry(entry = {}) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export function readCostLog(homeDir) {
|
|
85
|
-
const logPath = join(homeDir, '
|
|
86
|
+
const logPath = join(doctorRoot(homeDir), 'session-cost.jsonl');
|
|
86
87
|
if (!existsSync(logPath)) return [];
|
|
87
88
|
try {
|
|
88
89
|
return readFileSync(logPath, 'utf8')
|
|
@@ -97,7 +98,7 @@ export function readCostLog(homeDir) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
export function clearCostLog(homeDir) {
|
|
100
|
-
const logPath = join(homeDir, '
|
|
101
|
+
const logPath = join(doctorRoot(homeDir), 'session-cost.jsonl');
|
|
101
102
|
if (existsSync(logPath)) writeFileSync(logPath, '');
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* contracts as the single source of truth.
|
|
8
8
|
*
|
|
9
9
|
* Three decision sources are indexed:
|
|
10
|
-
* - ADRs docs/adr/*.md (status + supersedes parsed from body)
|
|
10
|
+
* - ADRs docs/decisions/adr/*.md (status + supersedes parsed from body)
|
|
11
11
|
* - Contracts specialists/contracts.json (executable postconditions = enforcement)
|
|
12
12
|
* - Rules rules/**\/*.md (enforcement link via frontmatter; bead wvbf.4)
|
|
13
13
|
*
|
|
@@ -37,7 +37,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
37
37
|
const ENFORCES_LINE_RE = /^(?:\/\/|#|\*)?\s*@enforces\s+(\S+)\s*$/;
|
|
38
38
|
|
|
39
39
|
// The body carries Status and Supersedes as markdown definition lines, not
|
|
40
|
-
// frontmatter (see docs/adr/templates/_template.md); parse them positionally.
|
|
40
|
+
// frontmatter (see docs/decisions/adr/templates/_template.md); parse them positionally.
|
|
41
41
|
|
|
42
42
|
// ADR field styles vary across the lane: inline bold (`- **Status**: x`,
|
|
43
43
|
// `**Status:** X`) and heading-then-value (`## Status` followed by the value on
|
|
@@ -121,7 +121,7 @@ function collectEnforcementBindings({ repoRoot }) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function adrDecisions({ repoRoot, bindings }) {
|
|
124
|
-
const dir = join(repoRoot, 'docs', 'adr');
|
|
124
|
+
const dir = join(repoRoot, 'docs', 'decisions', 'adr');
|
|
125
125
|
const out = [];
|
|
126
126
|
if (!existsSync(dir)) return out;
|
|
127
127
|
for (const name of readdirSync(dir)) {
|
package/lib/distill.mjs
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
|
|
10
10
|
import { join, extname, relative, dirname, basename } from 'path';
|
|
11
11
|
import { execSync, spawnSync } from 'child_process';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
12
|
import { getActiveOverlays } from './headhunt.mjs';
|
|
13
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
14
14
|
import { withGenAiSpan, GenAiAttrs } from './telemetry/otel-tracer.mjs';
|
|
15
15
|
import {
|
|
16
16
|
EXTRACTABLE_DOCUMENT_EXTS,
|
|
@@ -347,7 +347,7 @@ export async function distill(dir, { format = 'summary', query = '', mode = 'aut
|
|
|
347
347
|
|
|
348
348
|
const prompt = structured.prompt;
|
|
349
349
|
if (mode === 'prompt' || !hasClaude()) {
|
|
350
|
-
const tmpFile = out || join(
|
|
350
|
+
const tmpFile = out || join(doctorRoot(), 'distill-prompt.txt');
|
|
351
351
|
try { mkdirSync(dirname(tmpFile), { recursive: true }); } catch { /* exists */ }
|
|
352
352
|
writeFileSync(tmpFile, prompt);
|
|
353
353
|
process.stderr.write(`\n⚠ Prompt written to: ${tmpFile}\n`);
|
package/lib/docs-verify.mjs
CHANGED
|
@@ -210,7 +210,9 @@ function gitTrackedMdFiles(cwd, dir) {
|
|
|
210
210
|
|
|
211
211
|
function checkIntakeTraceability(cwd) {
|
|
212
212
|
const intakeFedDirs = [
|
|
213
|
+
path.join(cwd, 'docs', 'specs', 'prd'),
|
|
213
214
|
path.join(cwd, 'docs', 'prd'),
|
|
215
|
+
path.join(cwd, 'docs', 'notes', 'research'),
|
|
214
216
|
path.join(cwd, 'docs', 'research'),
|
|
215
217
|
path.join(cwd, '.cx', 'knowledge', 'internal'),
|
|
216
218
|
];
|
package/lib/doctor/audit.mjs
CHANGED
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
* what the L0 layer did while they weren't looking — and the basis for rolling
|
|
7
7
|
* back a misfire if rules are wrong.
|
|
8
8
|
*
|
|
9
|
-
* Path:
|
|
9
|
+
* Path: <doctorRoot>/doctor-log.jsonl (overridable via CONSTRUCT_DOCTOR_ROOT for tests).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
|
|
15
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
16
|
+
|
|
16
17
|
const MAX_LINES = 2000;
|
|
17
18
|
|
|
18
19
|
function rootDir() {
|
|
19
|
-
return
|
|
20
|
+
return doctorRoot();
|
|
20
21
|
}
|
|
21
22
|
function logPath() {
|
|
22
23
|
return join(rootDir(), 'doctor-log.jsonl');
|
package/lib/doctor/cli.mjs
CHANGED
package/lib/doctor/index.mjs
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
* never starves the others. Termination via SIGTERM/SIGINT triggers a clean
|
|
8
8
|
* shutdown that records a final audit entry.
|
|
9
9
|
*
|
|
10
|
-
* State file:
|
|
10
|
+
* State file: doctor.json in the XDG state dir (pid, started, lastTick).
|
|
11
11
|
* Audit log: ~/.cx/doctor-log.jsonl.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
15
|
import { join } from 'node:path';
|
|
17
16
|
|
|
18
17
|
import { record } from './audit.mjs';
|
|
@@ -26,8 +25,9 @@ import * as consistency from './watchers/consistency.mjs';
|
|
|
26
25
|
import * as mcpProtocol from './watchers/mcp-protocol.mjs';
|
|
27
26
|
import * as cxBudget from './watchers/cx-budget.mjs';
|
|
28
27
|
import * as graphStaleness from './watchers/graph-staleness.mjs';
|
|
28
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
29
29
|
|
|
30
|
-
const STATE_PATH = join(
|
|
30
|
+
const STATE_PATH = join(stateDir(), 'doctor.json');
|
|
31
31
|
const WATCHERS = [disk, cost, processPressure, serviceHealth, bdWatch, handoffs, consistency, mcpProtocol, cxBudget, graphStaleness];
|
|
32
32
|
|
|
33
33
|
let running = false;
|
|
@@ -35,7 +35,7 @@ let timers = [];
|
|
|
35
35
|
let lastTickAt = {};
|
|
36
36
|
|
|
37
37
|
function writeState(state) {
|
|
38
|
-
const dir =
|
|
38
|
+
const dir = stateDir();
|
|
39
39
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
40
40
|
writeFileSync(STATE_PATH, JSON.stringify({ ...state, updatedAt: Date.now() }));
|
|
41
41
|
}
|
package/lib/doctor/report.mjs
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
12
11
|
import { join } from 'node:path';
|
|
13
12
|
|
|
14
13
|
import { recent } from './audit.mjs';
|
|
14
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
15
15
|
import { getTotalDailySpend, getDailySpend, totalBudget, personaBudget, dayKey } from '../cost-ledger.mjs';
|
|
16
16
|
|
|
17
17
|
function parseWindow(arg) {
|
|
@@ -67,8 +67,8 @@ export async function runReport(args = []) {
|
|
|
67
67
|
const since = Date.now() - windowMs;
|
|
68
68
|
|
|
69
69
|
const auditEntries = recent({ since, limit: 10000 });
|
|
70
|
-
const events = readJsonl(join(
|
|
71
|
-
const pending = readJsonl(join(
|
|
70
|
+
const events = readJsonl(join(doctorRoot(), 'events.jsonl')).filter((e) => e.ts >= since);
|
|
71
|
+
const pending = readJsonl(join(doctorRoot(), 'role-pending.jsonl')).filter((e) => e.ts >= since);
|
|
72
72
|
|
|
73
73
|
const summary = summarizeActions(auditEntries);
|
|
74
74
|
const eventsByType = groupBy(events, (e) => e.type);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Every 5 min, polls bd for open issues labeled `next:cx-<role>` where the
|
|
5
5
|
* target persona is onboarded in the role framework. Newly-seen issues are
|
|
6
6
|
* enqueued as pending invocations so session-start surfaces them to
|
|
7
|
-
* Construct. Seen issues are remembered in
|
|
7
|
+
* Construct. Seen issues are remembered in <doctorRoot>/bd-watch-seen.json to
|
|
8
8
|
* prevent re-emission while the label remains.
|
|
9
9
|
*
|
|
10
10
|
* Closes a gap in agent-tracker: agent-tracker only catches handoffs that
|
|
@@ -16,20 +16,20 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
17
|
import { appendBounded } from '../../logging/rotate.mjs';
|
|
18
18
|
import { resolveProjectScope } from '../../project-root.mjs';
|
|
19
|
-
import { homedir } from 'node:os';
|
|
20
19
|
import { join } from 'node:path';
|
|
21
20
|
|
|
22
21
|
import { record } from '../audit.mjs';
|
|
23
22
|
import { listOnboardedPersonas, loadManifest } from '../../roles/manifest.mjs';
|
|
23
|
+
import { doctorRoot } from '../../config/xdg.mjs';
|
|
24
24
|
|
|
25
25
|
export const name = 'bd-watch';
|
|
26
26
|
export const intervalMs = 5 * 60 * 1000;
|
|
27
27
|
|
|
28
|
-
const SEEN_PATH = join(
|
|
29
|
-
const PENDING_PATH = join(
|
|
28
|
+
const SEEN_PATH = join(doctorRoot(), 'bd-watch-seen.json');
|
|
29
|
+
const PENDING_PATH = join(doctorRoot(), 'role-pending.jsonl');
|
|
30
30
|
|
|
31
31
|
function ensureDir() {
|
|
32
|
-
const dir =
|
|
32
|
+
const dir = doctorRoot();
|
|
33
33
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
|
|
16
15
|
import { record } from '../audit.mjs';
|
|
16
|
+
import { doctorRoot } from '../../config/xdg.mjs';
|
|
17
17
|
import {
|
|
18
18
|
getTotalDailySpend,
|
|
19
19
|
getDailySpend,
|
|
@@ -26,15 +26,15 @@ import {
|
|
|
26
26
|
export const name = 'cost';
|
|
27
27
|
export const intervalMs = 10 * 60 * 1000;
|
|
28
28
|
|
|
29
|
-
const SESSION_COST_PATH = join(
|
|
30
|
-
const STATE_PATH = join(
|
|
29
|
+
const SESSION_COST_PATH = join(doctorRoot(), 'session-cost.jsonl');
|
|
30
|
+
const STATE_PATH = join(doctorRoot(), 'cost-watcher-state.json');
|
|
31
31
|
|
|
32
32
|
function loadState() {
|
|
33
33
|
if (!existsSync(STATE_PATH)) return { lastIngestedTsMs: 0 };
|
|
34
34
|
try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')); } catch { return { lastIngestedTsMs: 0 }; }
|
|
35
35
|
}
|
|
36
36
|
function saveState(state) {
|
|
37
|
-
const dir =
|
|
37
|
+
const dir = doctorRoot();
|
|
38
38
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
39
|
writeFileSync(STATE_PATH, JSON.stringify(state));
|
|
40
40
|
}
|
|
@@ -16,6 +16,7 @@ import { join } from 'node:path';
|
|
|
16
16
|
|
|
17
17
|
import { record } from '../audit.mjs';
|
|
18
18
|
import { escalate } from '../escalate.mjs';
|
|
19
|
+
import { cacheDir } from '../../config/xdg.mjs';
|
|
19
20
|
|
|
20
21
|
export const name = 'disk';
|
|
21
22
|
export const intervalMs = 5 * 60 * 1000;
|
|
@@ -54,7 +55,7 @@ function rotateTarget({ path: relPath, maxLines }) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function pruneRuntimeLogs() {
|
|
57
|
-
const runtimeDir = join(
|
|
58
|
+
const runtimeDir = join(cacheDir(), '.runtime');
|
|
58
59
|
if (!existsSync(runtimeDir)) return { pruned: 0 };
|
|
59
60
|
const maxAgeMs = 7 * 24 * 60 * 60 * 1000;
|
|
60
61
|
const now = Date.now();
|
|
@@ -99,11 +100,11 @@ export async function tick() {
|
|
|
99
100
|
kind: 'action',
|
|
100
101
|
watcher: name,
|
|
101
102
|
action: 'prune',
|
|
102
|
-
target: '.construct/.runtime/',
|
|
103
|
+
target: '.cache/construct/.runtime/',
|
|
103
104
|
summary: `pruned ${runtimePrune.pruned} stale runtime log(s) (>7 days)`,
|
|
104
105
|
context: runtimePrune,
|
|
105
106
|
});
|
|
106
|
-
actions.push({ type: 'prune', target: '.construct/.runtime/' });
|
|
107
|
+
actions.push({ type: 'prune', target: '.cache/construct/.runtime/' });
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const free = freeBytesAt(homedir());
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import { spawnSync, spawn } from 'node:child_process';
|
|
15
15
|
import { existsSync, readFileSync, openSync, mkdirSync } from 'node:fs';
|
|
16
|
-
import { homedir } from 'node:os';
|
|
17
16
|
import { dirname, join } from 'node:path';
|
|
18
17
|
import { fileURLToPath } from 'node:url';
|
|
19
18
|
|
|
20
19
|
import { record } from '../audit.mjs';
|
|
21
20
|
import { escalate } from '../escalate.mjs';
|
|
21
|
+
import { configDir, stateDir, cacheDir } from '../../config/xdg.mjs';
|
|
22
22
|
|
|
23
23
|
export const name = 'service-health';
|
|
24
24
|
export const intervalMs = 60 * 1000;
|
|
@@ -56,7 +56,7 @@ function probeTcp(host, port, timeoutMs = 1500) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function probeDashboardPort() {
|
|
59
|
-
const statePath = join(
|
|
59
|
+
const statePath = join(stateDir(), 'dashboard.json');
|
|
60
60
|
if (!existsSync(statePath)) return true;
|
|
61
61
|
try {
|
|
62
62
|
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
@@ -68,7 +68,7 @@ function probeDashboardPort() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function probeMemoryHealth() {
|
|
71
|
-
const envPath = join(
|
|
71
|
+
const envPath = join(configDir(), 'config.env');
|
|
72
72
|
if (!existsSync(envPath)) return true;
|
|
73
73
|
try {
|
|
74
74
|
const env = readFileSync(envPath, 'utf8');
|
|
@@ -87,7 +87,7 @@ function dockerRestart(container) {
|
|
|
87
87
|
|
|
88
88
|
function detachSpawn(command, args, logName) {
|
|
89
89
|
try {
|
|
90
|
-
const runtimeDir = join(
|
|
90
|
+
const runtimeDir = join(cacheDir(), '.runtime');
|
|
91
91
|
if (!existsSync(runtimeDir)) mkdirSync(runtimeDir, { recursive: true });
|
|
92
92
|
const logPath = join(runtimeDir, logName);
|
|
93
93
|
const fd = openSync(logPath, 'a');
|
|
@@ -106,7 +106,7 @@ function respawnViaConstructUp() {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function respawnCm() {
|
|
109
|
-
const envPath = join(
|
|
109
|
+
const envPath = join(configDir(), 'config.env');
|
|
110
110
|
let port = '4231';
|
|
111
111
|
try {
|
|
112
112
|
if (existsSync(envPath)) {
|
package/lib/document-export.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Export **source** is Construct-authored markdown (typed artifacts). **Output**
|
|
7
7
|
* spans many distributable formats. Ingest (separate pipeline) accepts PDF, Office,
|
|
8
|
-
* email, AV, and plain text — see docs/reference/document-io.md.
|
|
8
|
+
* email, AV, and plain text — see docs/guides/reference/document-io.md.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import fs from 'node:fs';
|
package/lib/document-extract.mjs
CHANGED
|
@@ -345,8 +345,8 @@ function extractPdf(filePath) {
|
|
|
345
345
|
if (extracted.text.length < threshold) {
|
|
346
346
|
const ocrHint = process.env.CONSTRUCT_PDF_OCR_HINT || 'none';
|
|
347
347
|
const hintMsg = ocrHint !== 'none'
|
|
348
|
-
? `OCR backend hint: ${ocrHint}. See docs/intake/scanned-pdfs.md.`
|
|
349
|
-
: 'Set CONSTRUCT_PDF_OCR_HINT=tesseract|pdfocr to enable OCR. See docs/intake/scanned-pdfs.md.';
|
|
348
|
+
? `OCR backend hint: ${ocrHint}. See docs/guides/intake/scanned-pdfs.md.`
|
|
349
|
+
: 'Set CONSTRUCT_PDF_OCR_HINT=tesseract|pdfocr to enable OCR. See docs/guides/intake/scanned-pdfs.md.';
|
|
350
350
|
extracted.droppedInfo.push(makeDropInfo({
|
|
351
351
|
kind: 'scanned-pdf',
|
|
352
352
|
count: pages,
|
|
@@ -692,7 +692,7 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
692
692
|
// Throw a typed error so callers can route to .cx/intake/needs-asr/.
|
|
693
693
|
if (AUDIO_VIDEO_EXTS.has(extension)) {
|
|
694
694
|
const err = new Error(
|
|
695
|
-
`Audio/video extraction requires ASR. Set CONSTRUCT_ASR_BACKEND=whisper|assemblyai to enable. See docs/intake/audio-video.md.`,
|
|
695
|
+
`Audio/video extraction requires ASR. Set CONSTRUCT_ASR_BACKEND=whisper|assemblyai to enable. See docs/guides/intake/audio-video.md.`,
|
|
696
696
|
);
|
|
697
697
|
err.code = 'ASR_REQUIRED';
|
|
698
698
|
err.extension = extension;
|