@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
|
@@ -51,7 +51,7 @@ tokens -> dash
|
|
|
51
51
|
|
|
52
52
|
## Decision
|
|
53
53
|
|
|
54
|
-
Adopt `lib/brand-tokens.mjs` as the single source of truth for visual brand primitives. Document naming, voice, tone, and profile rebrand in `docs/reference/branding.md`. Enforce retired-font drift with `scripts/audit/03d-brand.mjs` in the audit ratchet.
|
|
54
|
+
Adopt `lib/brand-tokens.mjs` as the single source of truth for visual brand primitives. Document naming, voice, tone, and profile rebrand in `docs/guides/reference/branding.md`. Enforce retired-font drift with `scripts/audit/03d-brand.mjs` in the audit ratchet.
|
|
55
55
|
|
|
56
56
|
## Rationale
|
|
57
57
|
|
|
@@ -79,6 +79,6 @@ Two-way door for documentation and audit scope; one-way for shipped PDFs already
|
|
|
79
79
|
|
|
80
80
|
## References
|
|
81
81
|
|
|
82
|
-
- `docs/reference/branding.md`
|
|
82
|
+
- `docs/guides/reference/branding.md`
|
|
83
83
|
- `lib/brand-tokens.mjs`
|
|
84
84
|
- https://fonts.google.com/specimen/Space+Grotesk (accessed 2026-06-22)
|
|
@@ -60,6 +60,6 @@ One token set across all distributable formats:
|
|
|
60
60
|
|
|
61
61
|
- Export: `construct export … --to=pptx`
|
|
62
62
|
- Regenerate: `npm run examples:deck`
|
|
63
|
-
- Matrix: [Document I/O reference](/reference/document-io)
|
|
63
|
+
- Matrix: [Document I/O reference](/guides/reference/document-io)
|
|
64
64
|
|
|
65
65
|
Review outputs in `.tmp/distribution-examples/`.
|
|
@@ -157,5 +157,5 @@ https://langchain-ai.github.io/langgraph/ (accessed 2026-06-19).
|
|
|
157
157
|
|
|
158
158
|
- Construct artifact manifest: `specialists/artifact-manifest.json`
|
|
159
159
|
- PRD workflow skill: `skills/docs/prd-workflow.md`
|
|
160
|
-
- Publish cookbook: `docs/cookbook/diagram-and-demo.md`
|
|
160
|
+
- Publish cookbook: `docs/guides/cookbook/diagram-and-demo.md`
|
|
161
161
|
- Golden D2 sources: `tests/fixtures/publish/diagrams/`
|
|
@@ -32,7 +32,7 @@ Searched arXiv and vendor docs (2024–2026), reviewed three internal ADRs on in
|
|
|
32
32
|
|---|---|---|---|---|---|---|---|
|
|
33
33
|
| Multi-agent survey | secondary | B | 3 | 2023-08-01 | https://arxiv.org/abs/2308.08155 | yes | Patterns for specialist routing |
|
|
34
34
|
| LangGraph overview | primary | A | 4 | 2025-01-15 | https://langchain-ai.github.io/langgraph/ | yes | Stateful agent graphs |
|
|
35
|
-
| Construct architecture | primary | A | 5 | 2026-06-01 | docs/concepts/architecture.mdx | yes | Internal contract |
|
|
35
|
+
| Construct architecture | primary | A | 5 | 2026-06-01 | docs/guides/concepts/architecture.mdx | yes | Internal contract |
|
|
36
36
|
|
|
37
37
|
## Findings
|
|
38
38
|
|
|
@@ -77,4 +77,4 @@ Ship distribution examples (`npm run examples:distribution`) so evaluators can c
|
|
|
77
77
|
## References
|
|
78
78
|
|
|
79
79
|
- https://arxiv.org/abs/2308.08155 (accessed 2026-06-22)
|
|
80
|
-
- `docs/cookbook/diagram-and-demo.md`
|
|
80
|
+
- `docs/guides/cookbook/diagram-and-demo.md`
|
|
@@ -99,5 +99,5 @@ Preserve `construct tools detect --json` output and the failing pandoc/typst std
|
|
|
99
99
|
|
|
100
100
|
## References
|
|
101
101
|
|
|
102
|
-
- `docs/reference/document-io.md`
|
|
103
|
-
- `docs/cookbook/diagram-and-demo.md`
|
|
102
|
+
- `docs/guides/reference/document-io.md`
|
|
103
|
+
- `docs/guides/cookbook/diagram-and-demo.md`
|
|
@@ -26,7 +26,7 @@ Host-native agents optimize for single-session chat. Enterprise platform teams n
|
|
|
26
26
|
## Goals
|
|
27
27
|
|
|
28
28
|
1. **Prove value in five minutes** — `npm run examples:distribution` produces a browsable gallery with figures.
|
|
29
|
-
2. **One brand contract** — `docs/reference/branding.md` + `03d-brand` audit ratchet.
|
|
29
|
+
2. **One brand contract** — `docs/guides/reference/branding.md` + `03d-brand` audit ratchet.
|
|
30
30
|
3. **Profile-aware language** — intake queue labels follow active profile rebrand in CLI, session prelude, and dashboard API.
|
|
31
31
|
|
|
32
32
|
## Non-goals
|
|
@@ -71,7 +71,7 @@ export function buildConstructIntegrationBody({ hasBeadsBlock = false, variant =
|
|
|
71
71
|
'- **Durable state** lives in `.cx/` (context, knowledge, intake, traces) and Beads.',
|
|
72
72
|
' If services are down, resume from `plan.md`, `.cx/context.md`, the latest',
|
|
73
73
|
' `.cx/handoffs/` file, Beads, and git.',
|
|
74
|
-
'- **Signals**: drop a file into
|
|
74
|
+
'- **Signals**: drop a file into `inbox/`; `construct intake` classifies and routes it.',
|
|
75
75
|
'- **Specialists** (architect, reviewer, security, …) are dispatched by Construct — you',
|
|
76
76
|
' do not call them directly.',
|
|
77
77
|
tracker,
|
|
@@ -14,6 +14,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
15
|
const EMPTY_MANIFEST = { version: 1, artifacts: {} };
|
|
16
16
|
|
|
17
|
+
// Kept here, next to the manifest resolver, so every public workflow surface
|
|
18
|
+
// uses one answer for what can carry Construct styling. Source formats remain
|
|
19
|
+
// deliberately plain unless a caller explicitly asks otherwise.
|
|
20
|
+
export const BRAND_CAPABLE_FORMATS = Object.freeze(['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex']);
|
|
21
|
+
export const PLAIN_OUTPUT_FORMATS = Object.freeze(['txt', 'md', 'mdx']);
|
|
22
|
+
|
|
17
23
|
let cached = null;
|
|
18
24
|
let cachedRoot = null;
|
|
19
25
|
|
|
@@ -60,6 +66,140 @@ export function artifactTypes(opts = {}) {
|
|
|
60
66
|
return Object.keys(manifest.artifacts ?? {});
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
function isObject(value) {
|
|
70
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mergeObjects(...values) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
if (!isObject(value)) continue;
|
|
77
|
+
for (const [key, next] of Object.entries(value)) {
|
|
78
|
+
out[key] = isObject(next) && isObject(out[key]) ? mergeObjects(out[key], next) : structuredClone(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function unique(items) {
|
|
85
|
+
return [...new Set(items.filter(Boolean))];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a user-facing type or registered alias. The result intentionally
|
|
90
|
+
* distinguishes an unknown request from a fallback: callers must ask for
|
|
91
|
+
* classification/registration instead of silently turning it into a PRD.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveArtifactType(type, opts = {}) {
|
|
94
|
+
const requestedType = String(type ?? '').trim().toLowerCase();
|
|
95
|
+
const manifest = loadArtifactManifest(opts);
|
|
96
|
+
if (!requestedType) {
|
|
97
|
+
return {
|
|
98
|
+
status: 'unrecognized',
|
|
99
|
+
requestedType: null,
|
|
100
|
+
type: null,
|
|
101
|
+
guidance: 'Specify a registered document class or register one in specialists/artifact-manifest.json.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
for (const [registeredType, entry] of Object.entries(manifest.artifacts ?? {})) {
|
|
105
|
+
const aliases = [registeredType, ...(entry.aliases ?? [])].map((value) => String(value).toLowerCase());
|
|
106
|
+
if (aliases.includes(requestedType)) {
|
|
107
|
+
return { status: 'registered', requestedType, type: registeredType, entry };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
status: 'unrecognized',
|
|
112
|
+
requestedType,
|
|
113
|
+
type: null,
|
|
114
|
+
guidance: `Document class '${requestedType}' is not registered. Classify the request or add it to specialists/artifact-manifest.json.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the canonical workflow contract for a registered document class.
|
|
120
|
+
* Precedence is deliberately explicit: invocation > project config > manifest
|
|
121
|
+
* defaults. Existing entries remain compatible: primaryOwners and releaseGate
|
|
122
|
+
* are the fallback author and reviewer/validation declarations.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveArtifactWorkflowContract(type, {
|
|
125
|
+
rootDir,
|
|
126
|
+
cwd = process.cwd(),
|
|
127
|
+
projectConfig = null,
|
|
128
|
+
overrides = null,
|
|
129
|
+
} = {}) {
|
|
130
|
+
const resolved = resolveArtifactType(type, { rootDir, cwd });
|
|
131
|
+
if (resolved.status !== 'registered') return resolved;
|
|
132
|
+
|
|
133
|
+
const manifest = loadArtifactManifest({ rootDir, cwd });
|
|
134
|
+
const entry = resolved.entry;
|
|
135
|
+
const config = projectConfig?.artifactWorkflow ?? {};
|
|
136
|
+
const manifestDefaults = manifest.workflowDefaults ?? {};
|
|
137
|
+
const projectType = config.types?.[resolved.type] ?? {};
|
|
138
|
+
const appliedOverrides = [];
|
|
139
|
+
if (isObject(config.defaults) && Object.keys(config.defaults).length) appliedOverrides.push('project.defaults');
|
|
140
|
+
if (isObject(projectType) && Object.keys(projectType).length) appliedOverrides.push(`project.types.${resolved.type}`);
|
|
141
|
+
if (isObject(overrides) && Object.keys(overrides).length) appliedOverrides.push('invocation');
|
|
142
|
+
|
|
143
|
+
const configured = mergeObjects(manifestDefaults, entry, config.defaults, projectType, overrides);
|
|
144
|
+
const releaseGate = configured.releaseGate ?? {};
|
|
145
|
+
const authorChain = unique(configured.authorChain ?? configured.primaryOwners ?? []);
|
|
146
|
+
const reviewerChain = unique(configured.reviewerChain ?? [
|
|
147
|
+
...(releaseGate.requiredReviewers ?? []),
|
|
148
|
+
...(releaseGate.optionalReviewers ?? []),
|
|
149
|
+
]);
|
|
150
|
+
const outputs = mergeObjects({
|
|
151
|
+
formats: [...BRAND_CAPABLE_FORMATS, ...PLAIN_OUTPUT_FORMATS],
|
|
152
|
+
branding: 'construct',
|
|
153
|
+
}, configured.outputs);
|
|
154
|
+
const validation = mergeObjects({ releaseGate: true }, configured.validation);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
status: 'registered',
|
|
158
|
+
requestedType: resolved.requestedType,
|
|
159
|
+
type: resolved.type,
|
|
160
|
+
documentClass: configured.documentClass ?? resolved.type,
|
|
161
|
+
template: configured.template ?? null,
|
|
162
|
+
workflowSkill: configured.workflowSkill ?? null,
|
|
163
|
+
authorChain,
|
|
164
|
+
reviewerChain,
|
|
165
|
+
requiredReviewers: unique(releaseGate.requiredReviewers ?? []),
|
|
166
|
+
optionalReviewers: unique(releaseGate.optionalReviewers ?? []),
|
|
167
|
+
researchProfile: configured.researchProfile ?? null,
|
|
168
|
+
validation,
|
|
169
|
+
releaseGate,
|
|
170
|
+
outputs,
|
|
171
|
+
appliedOverrides,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A dependency-free validator used by startup checks and tests. */
|
|
176
|
+
export function validateArtifactManifest(manifest) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (!isObject(manifest)) return { valid: false, errors: ['root: must be an object'] };
|
|
179
|
+
if (!Number.isInteger(manifest.version) || manifest.version < 1) errors.push('version: must be an integer >= 1');
|
|
180
|
+
if (!isObject(manifest.artifacts)) errors.push('artifacts: must be an object');
|
|
181
|
+
for (const [type, entry] of Object.entries(manifest.artifacts ?? {})) {
|
|
182
|
+
const prefix = `artifacts.${type}`;
|
|
183
|
+
if (!isObject(entry)) { errors.push(`${prefix}: must be an object`); continue; }
|
|
184
|
+
if (typeof entry.template !== 'string' || !entry.template) errors.push(`${prefix}.template: required string missing`);
|
|
185
|
+
if (!Array.isArray(entry.primaryOwners) || entry.primaryOwners.some((owner) => typeof owner !== 'string')) {
|
|
186
|
+
errors.push(`${prefix}.primaryOwners: required string array missing`);
|
|
187
|
+
}
|
|
188
|
+
for (const field of ['authorChain', 'reviewerChain', 'aliases']) {
|
|
189
|
+
if (entry[field] !== undefined && (!Array.isArray(entry[field]) || entry[field].some((value) => typeof value !== 'string'))) {
|
|
190
|
+
errors.push(`${prefix}.${field}: must be an array of strings`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (entry.outputs?.formats !== undefined && (!Array.isArray(entry.outputs.formats) || entry.outputs.formats.length === 0)) {
|
|
194
|
+
errors.push(`${prefix}.outputs.formats: must be a non-empty array`);
|
|
195
|
+
}
|
|
196
|
+
if (entry.outputs?.branding !== undefined && !['construct', 'plain'].includes(entry.outputs.branding)) {
|
|
197
|
+
errors.push(`${prefix}.outputs.branding: must be construct or plain`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return errors.length ? { valid: false, errors } : { valid: true, errors: [] };
|
|
201
|
+
}
|
|
202
|
+
|
|
63
203
|
export function structureRequirementsFromManifest(opts = {}) {
|
|
64
204
|
const manifest = loadArtifactManifest(opts);
|
|
65
205
|
const out = {};
|
|
@@ -97,16 +237,22 @@ export function resolveToneForArtifact(type, { cwd = process.cwd(), rootDir } =
|
|
|
97
237
|
}
|
|
98
238
|
|
|
99
239
|
export function templateMetadata(type, { cwd = process.cwd(), rootDir } = {}) {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
240
|
+
const workflow = resolveArtifactWorkflowContract(type, { rootDir, cwd });
|
|
241
|
+
if (workflow.status !== 'registered') return null;
|
|
242
|
+
const entry = getArtifactEntry(workflow.type, { rootDir, cwd });
|
|
102
243
|
return {
|
|
103
|
-
type,
|
|
104
|
-
|
|
244
|
+
type: workflow.type,
|
|
245
|
+
documentClass: workflow.documentClass,
|
|
246
|
+
tone: resolveToneForArtifact(workflow.type, { cwd, rootDir }),
|
|
105
247
|
toneAllowed: entry.toneAllowed ?? [],
|
|
106
248
|
structureRequirements: entry.structureRequirements ?? [],
|
|
107
249
|
visualRequirements: entry.visualRequirements ?? [],
|
|
108
250
|
primaryOwners: entry.primaryOwners ?? [],
|
|
109
251
|
workflowSkill: entry.workflowSkill ?? null,
|
|
110
252
|
releaseGate: entry.releaseGate ?? null,
|
|
253
|
+
authorChain: workflow.authorChain,
|
|
254
|
+
reviewerChain: workflow.reviewerChain,
|
|
255
|
+
validation: workflow.validation,
|
|
256
|
+
outputs: workflow.outputs,
|
|
111
257
|
};
|
|
112
258
|
}
|
|
@@ -41,12 +41,16 @@ function inferRfcSubtype(rel, base) {
|
|
|
41
41
|
return 'rfc';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Match both this repo's bucketed layout (docs/specs/prd, docs/decisions/adr, …)
|
|
45
|
+
// and the init-lane layout that `construct init` scaffolds downstream (docs/prd,
|
|
46
|
+
// docs/adr, …), so artifact-type inference works in either project shape.
|
|
47
|
+
|
|
44
48
|
const PREFIX_RULES = [
|
|
45
|
-
[/^docs\/prd\//, inferPrdSubtype],
|
|
46
|
-
[/^docs\/adr\//, () => 'adr'],
|
|
47
|
-
[/^docs\/rfc\//, inferRfcSubtype],
|
|
48
|
-
[/^docs\/research\//, () => 'research-brief'],
|
|
49
|
-
[/^docs\/runbooks?\//, () => 'runbook'],
|
|
49
|
+
[/^docs\/(?:specs\/)?prd\//, inferPrdSubtype],
|
|
50
|
+
[/^docs\/(?:decisions\/)?adr\//, () => 'adr'],
|
|
51
|
+
[/^docs\/(?:decisions\/)?rfc\//, inferRfcSubtype],
|
|
52
|
+
[/^docs\/(?:notes\/)?research\//, () => 'research-brief'],
|
|
53
|
+
[/^docs\/(?:operations\/)?runbooks?\//, () => 'runbook'],
|
|
50
54
|
[/^\.cx\/research\//, () => 'research-brief'],
|
|
51
55
|
];
|
|
52
56
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/artifact-workflow.mjs — intent-to-artifact workflow planning.
|
|
3
|
+
*
|
|
4
|
+
* Planning describes specialist work and manifest ordering without reporting a
|
|
5
|
+
* planned role as executed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { loadProjectConfig } from './config/project-config.mjs';
|
|
10
|
+
import {
|
|
11
|
+
BRAND_CAPABLE_FORMATS,
|
|
12
|
+
artifactTypes,
|
|
13
|
+
resolveArtifactType,
|
|
14
|
+
resolveArtifactWorkflowContract,
|
|
15
|
+
} from './artifact-manifest.mjs';
|
|
16
|
+
import { inferArtifactTypeFromPath } from './artifact-type-from-path.mjs';
|
|
17
|
+
import { validateArtifactRelease } from './artifact-release-gate.mjs';
|
|
18
|
+
import { exportMarkdown } from './document-export.mjs';
|
|
19
|
+
|
|
20
|
+
function escapeRegex(value) {
|
|
21
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findRequestedTypes(input, { rootDir, cwd }) {
|
|
25
|
+
const text = String(input ?? '').toLowerCase();
|
|
26
|
+
const matches = [];
|
|
27
|
+
for (const type of artifactTypes({ rootDir, cwd }).sort((a, b) => b.length - a.length)) {
|
|
28
|
+
const entry = resolveArtifactType(type, { rootDir, cwd }).entry;
|
|
29
|
+
for (const candidate of [type, ...(entry?.aliases ?? [])]) {
|
|
30
|
+
if (new RegExp(`(?:^|[^a-z0-9])${escapeRegex(candidate.toLowerCase())}(?:$|[^a-z0-9])`, 'i').test(text)) {
|
|
31
|
+
matches.push(type);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...new Set(matches)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function inferFormat(input, requestedFormat) {
|
|
40
|
+
if (requestedFormat) return String(requestedFormat).toLowerCase();
|
|
41
|
+
const text = String(input ?? '').toLowerCase();
|
|
42
|
+
for (const format of ['pptx', 'docx', 'pdf', 'deck', 'html', 'doc', 'rtf', 'odt', 'epub', 'tex', 'txt', 'mdx', 'md']) {
|
|
43
|
+
if (new RegExp(`\\b${format}\\b`, 'i').test(text)) return format;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function inferBranding(input, requestedBranding) {
|
|
49
|
+
if (requestedBranding) return requestedBranding;
|
|
50
|
+
return /\b(?:unbranded|plain|without (?:the )?construct brand)\b/i.test(String(input ?? '')) ? 'plain' : 'construct';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildSteps({ contract, input, targetFormat, branding }) {
|
|
54
|
+
const text = String(input ?? '').toLowerCase();
|
|
55
|
+
const needsResearch = Boolean(contract.researchProfile) && /\b(?:research|source|evidence|validate claims)\b/.test(text);
|
|
56
|
+
const needsReview = /\b(?:review|critique|audit|feedback)\b/.test(text) || contract.requiredReviewers.length > 0;
|
|
57
|
+
const rewrite = /\b(?:rewrite|revise|edit|polish|improve)\b/.test(text);
|
|
58
|
+
const steps = [];
|
|
59
|
+
if (needsResearch) steps.push({ id: 'research', action: 'research', roles: [], required: false, reason: `research profile: ${contract.researchProfile}` });
|
|
60
|
+
if (needsReview) steps.push({ id: 'review', action: 'review', roles: contract.reviewerChain, required: contract.requiredReviewers.length > 0, reason: 'review occurs before any rewrite' });
|
|
61
|
+
steps.push({
|
|
62
|
+
id: rewrite ? 'rewrite' : 'author',
|
|
63
|
+
action: rewrite ? 'rewrite' : 'author',
|
|
64
|
+
roles: contract.authorChain,
|
|
65
|
+
required: true,
|
|
66
|
+
reason: rewrite ? 'requested revision after review' : 'manifest author chain',
|
|
67
|
+
});
|
|
68
|
+
if (contract.validation.releaseGate !== false) {
|
|
69
|
+
steps.push({ id: 'validate', action: 'validate', roles: [], required: true, reason: 'manifest validation policy' });
|
|
70
|
+
}
|
|
71
|
+
if (targetFormat && BRAND_CAPABLE_FORMATS.includes(targetFormat) && branding === 'construct') {
|
|
72
|
+
steps.push({ id: 'brand', action: 'brand', roles: [], required: true, reason: 'default Construct branding for brand-capable format' });
|
|
73
|
+
}
|
|
74
|
+
if (targetFormat) steps.push({ id: 'export', action: 'export', roles: [], required: true, format: targetFormat, reason: 'requested distribution format' });
|
|
75
|
+
return steps;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build an artifact plan from an ordinary request. A missing or ambiguous
|
|
80
|
+
* class is a clarification result, never an implicit PRD route.
|
|
81
|
+
*/
|
|
82
|
+
export function planArtifactWorkflow(request = {}, { cwd = process.cwd(), rootDir } = {}) {
|
|
83
|
+
const input = request.input ?? request.request ?? '';
|
|
84
|
+
const sourcePath = request.sourcePath ?? request.filePath ?? '';
|
|
85
|
+
const project = request.projectConfig ?? loadProjectConfig(cwd).config;
|
|
86
|
+
const requested = request.artifactType ?? request.documentClass ?? null;
|
|
87
|
+
const pathType = sourcePath && existsSync(sourcePath) ? inferArtifactTypeFromPath(sourcePath, { rootDir: cwd }) : null;
|
|
88
|
+
const inferredTypes = requested ? [] : findRequestedTypes(input, { rootDir, cwd });
|
|
89
|
+
const candidates = requested ? [requested] : [...new Set([pathType, ...inferredTypes].filter(Boolean))];
|
|
90
|
+
|
|
91
|
+
if (candidates.length !== 1) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'artifact-workflow',
|
|
94
|
+
status: 'needs-classification',
|
|
95
|
+
plannedSteps: [],
|
|
96
|
+
executedSteps: [],
|
|
97
|
+
skippedSteps: [],
|
|
98
|
+
producedFiles: [],
|
|
99
|
+
validation: null,
|
|
100
|
+
appliedOverrides: [],
|
|
101
|
+
clarification: candidates.length > 1
|
|
102
|
+
? `Request names multiple document classes (${candidates.join(', ')}). Specify one registered class.`
|
|
103
|
+
: 'Specify a registered document class, use a typed source path, or register the requested class in specialists/artifact-manifest.json.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const resolution = resolveArtifactType(candidates[0], { rootDir, cwd });
|
|
108
|
+
if (resolution.status !== 'registered') {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'artifact-workflow',
|
|
111
|
+
status: 'needs-classification',
|
|
112
|
+
plannedSteps: [],
|
|
113
|
+
executedSteps: [],
|
|
114
|
+
skippedSteps: [],
|
|
115
|
+
producedFiles: [],
|
|
116
|
+
validation: null,
|
|
117
|
+
appliedOverrides: [],
|
|
118
|
+
clarification: resolution.guidance,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const targetFormat = inferFormat(input, request.format ?? request.targetFormat);
|
|
123
|
+
const requestedBranding = inferBranding(input, request.branding);
|
|
124
|
+
const contract = resolveArtifactWorkflowContract(resolution.type, {
|
|
125
|
+
rootDir,
|
|
126
|
+
cwd,
|
|
127
|
+
projectConfig: project,
|
|
128
|
+
overrides: request.overrides,
|
|
129
|
+
});
|
|
130
|
+
const branding = request.branding ?? (requestedBranding === 'plain' ? 'plain' : (contract.outputs.branding ?? 'construct'));
|
|
131
|
+
const steps = buildSteps({ contract, input, targetFormat, branding });
|
|
132
|
+
const unavailableFormat = targetFormat && !contract.outputs.formats.includes(targetFormat);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
kind: 'artifact-workflow',
|
|
136
|
+
status: unavailableFormat ? 'needs-classification' : 'planned',
|
|
137
|
+
documentClass: contract.documentClass,
|
|
138
|
+
artifactType: contract.type,
|
|
139
|
+
sourcePath: sourcePath || null,
|
|
140
|
+
target: {
|
|
141
|
+
format: targetFormat,
|
|
142
|
+
audience: /\bcustomer(?:-facing)?\b/i.test(input) ? 'customer' : 'internal',
|
|
143
|
+
branding,
|
|
144
|
+
brandCapable: Boolean(targetFormat && BRAND_CAPABLE_FORMATS.includes(targetFormat)),
|
|
145
|
+
},
|
|
146
|
+
contract,
|
|
147
|
+
plannedSteps: steps,
|
|
148
|
+
executedSteps: [],
|
|
149
|
+
skippedSteps: [],
|
|
150
|
+
producedFiles: [],
|
|
151
|
+
validation: null,
|
|
152
|
+
appliedOverrides: contract.appliedOverrides,
|
|
153
|
+
clarification: unavailableFormat ? `${targetFormat} is not an output capability for ${contract.type}.` : null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Produce a provenance-preserving workflow run report. Specialist authoring
|
|
159
|
+
* and review are never fabricated: Construct can plan them, but only reports
|
|
160
|
+
* them as executed when a host returns execution evidence (not available in
|
|
161
|
+
* the deterministic entry point). Validation/export are local operations and
|
|
162
|
+
* run only with the explicit durable-write approval mode.
|
|
163
|
+
*/
|
|
164
|
+
export function runArtifactWorkflow(request = {}, { cwd = process.cwd(), rootDir } = {}) {
|
|
165
|
+
const plan = planArtifactWorkflow(request, { cwd, rootDir });
|
|
166
|
+
const approvalMode = request.approvalMode ?? 'proposal-only';
|
|
167
|
+
const report = {
|
|
168
|
+
reportVersion: 1,
|
|
169
|
+
kind: 'artifact-workflow-run',
|
|
170
|
+
status: plan.status === 'planned' ? 'planned' : plan.status,
|
|
171
|
+
approval: {
|
|
172
|
+
mode: approvalMode,
|
|
173
|
+
durableWriteAllowed: approvalMode === 'allow-durable-write',
|
|
174
|
+
},
|
|
175
|
+
plan,
|
|
176
|
+
plannedSteps: plan.plannedSteps,
|
|
177
|
+
executedSteps: [],
|
|
178
|
+
skippedSteps: [],
|
|
179
|
+
producedFiles: [],
|
|
180
|
+
validation: null,
|
|
181
|
+
appliedOverrides: plan.appliedOverrides,
|
|
182
|
+
};
|
|
183
|
+
if (plan.status !== 'planned') return report;
|
|
184
|
+
|
|
185
|
+
const pendingReason = approvalMode === 'allow-durable-write'
|
|
186
|
+
? 'specialist execution is host-owned; this local command has no specialist execution evidence'
|
|
187
|
+
: `approval mode '${approvalMode}' permits planning only`;
|
|
188
|
+
for (const step of plan.plannedSteps) {
|
|
189
|
+
if (['review', 'rewrite', 'author', 'research'].includes(step.action)) {
|
|
190
|
+
report.skippedSteps.push({ id: step.id, reason: pendingReason });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (approvalMode !== 'allow-durable-write') {
|
|
194
|
+
for (const step of plan.plannedSteps) {
|
|
195
|
+
if (!['review', 'rewrite', 'author', 'research'].includes(step.action)) {
|
|
196
|
+
report.skippedSteps.push({ id: step.id, reason: pendingReason });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return report;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sourcePath = plan.sourcePath;
|
|
203
|
+
for (const step of plan.plannedSteps) {
|
|
204
|
+
if (step.action === 'validate') {
|
|
205
|
+
if (!sourcePath) {
|
|
206
|
+
report.skippedSteps.push({ id: step.id, reason: 'validation needs sourcePath or filePath' });
|
|
207
|
+
} else {
|
|
208
|
+
const result = validateArtifactRelease({ filePath: sourcePath, type: plan.artifactType, cwd, rootDir });
|
|
209
|
+
report.validation = result;
|
|
210
|
+
report.executedSteps.push({ id: step.id, result: result.ok ? 'passed' : 'failed' });
|
|
211
|
+
if (!result.ok) {
|
|
212
|
+
report.status = 'validation-failed';
|
|
213
|
+
return report;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (step.action === 'brand') {
|
|
218
|
+
// Branding is applied by the export operation. Keeping this pending until
|
|
219
|
+
// output exists avoids claiming a template was applied when no export ran.
|
|
220
|
+
report.skippedSteps.push({ id: step.id, reason: 'branding is evidenced by the export result' });
|
|
221
|
+
}
|
|
222
|
+
if (step.action === 'export') {
|
|
223
|
+
if (!sourcePath) {
|
|
224
|
+
report.skippedSteps.push({ id: step.id, reason: 'export needs sourcePath or filePath' });
|
|
225
|
+
} else {
|
|
226
|
+
const result = exportMarkdown({
|
|
227
|
+
inputPath: sourcePath,
|
|
228
|
+
outputPath: request.outputPath,
|
|
229
|
+
format: step.format,
|
|
230
|
+
artifactType: plan.artifactType,
|
|
231
|
+
branding: plan.target.branding,
|
|
232
|
+
cwd,
|
|
233
|
+
rootDir,
|
|
234
|
+
});
|
|
235
|
+
if (!result.ok) {
|
|
236
|
+
report.status = 'export-failed';
|
|
237
|
+
report.skippedSteps.push({ id: step.id, reason: result.message });
|
|
238
|
+
return report;
|
|
239
|
+
}
|
|
240
|
+
report.executedSteps.push({ id: step.id, result: 'produced', format: step.format });
|
|
241
|
+
report.producedFiles.push({ path: result.outputPath, format: step.format, branding: result.branding });
|
|
242
|
+
const brandStep = report.skippedSteps.findIndex((entry) => entry.id === 'brand');
|
|
243
|
+
if (brandStep >= 0 && result.branding?.applied === 'construct') {
|
|
244
|
+
report.skippedSteps.splice(brandStep, 1);
|
|
245
|
+
report.executedSteps.push({ id: 'brand', result: 'applied', mechanism: result.branding.mechanism });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
report.status = report.executedSteps.length ? 'completed-local-steps' : 'planned';
|
|
251
|
+
return report;
|
|
252
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Merges live registry, contracts, skills, prompts, and audit-enrichments into
|
|
5
5
|
* one machine-readable matrix. Powers `construct audit specialists` and the
|
|
6
|
-
* human-readable docs/concepts/specialist-skill-audit.md companion.
|
|
6
|
+
* human-readable docs/guides/concepts/specialist-skill-audit.md companion.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs';
|
package/lib/audit-trail.mjs
CHANGED
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
* Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
|
|
17
17
|
* the damaged prefix as immutable legacy and re-bases the live chain;
|
|
18
18
|
* verifyChain validates only from the last boundary forward. See
|
|
19
|
-
* docs/concepts/observability.md.
|
|
19
|
+
* docs/guides/concepts/observability.md.
|
|
20
20
|
*/
|
|
21
21
|
import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
|
|
22
22
|
import { createHash } from 'node:crypto';
|
|
23
23
|
import { join, dirname } from 'node:path';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
|
|
26
25
|
import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
|
|
27
26
|
import { withFileLockSync } from './storage/file-lock.mjs';
|
|
27
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
28
28
|
|
|
29
|
-
const AUDIT_FILE = join(
|
|
29
|
+
const AUDIT_FILE = join(doctorRoot(), 'audit-trail.jsonl');
|
|
30
30
|
|
|
31
31
|
function sha256(input) {
|
|
32
32
|
return createHash('sha256').update(input).digest('hex');
|