@geraldmaron/construct 1.0.6 → 1.0.7
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 +19 -17
- package/agents/contracts.json +617 -2
- package/agents/contracts.schema.json +83 -0
- package/agents/prompts/cx-accessibility.md +2 -0
- package/agents/prompts/cx-ai-engineer.md +2 -0
- package/agents/prompts/cx-architect.md +2 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +2 -0
- package/agents/prompts/cx-data-engineer.md +2 -0
- package/agents/prompts/cx-debugger.md +2 -0
- package/agents/prompts/cx-designer.md +2 -0
- package/agents/prompts/cx-devil-advocate.md +2 -0
- package/agents/prompts/cx-docs-keeper.md +2 -0
- package/agents/prompts/cx-engineer.md +2 -0
- package/agents/prompts/cx-evaluator.md +2 -0
- package/agents/prompts/cx-explorer.md +2 -0
- package/agents/prompts/cx-legal-compliance.md +2 -0
- package/agents/prompts/cx-operations.md +2 -0
- package/agents/prompts/cx-orchestrator.md +2 -0
- package/agents/prompts/cx-platform-engineer.md +2 -0
- package/agents/prompts/cx-product-manager.md +2 -0
- package/agents/prompts/cx-qa.md +2 -0
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-release-manager.md +2 -0
- package/agents/prompts/cx-researcher.md +2 -0
- package/agents/prompts/cx-reviewer.md +2 -0
- package/agents/prompts/cx-security.md +2 -0
- package/agents/prompts/cx-sre.md +2 -0
- package/agents/prompts/cx-test-automation.md +2 -0
- package/agents/prompts/cx-trace-reviewer.md +2 -0
- package/agents/prompts/cx-ux-researcher.md +2 -0
- package/agents/registry.json +8 -3
- package/bin/construct +209 -32
- package/commands/build/feature.md +1 -1
- package/commands/build/fix.md +1 -1
- package/commands/design/access.md +1 -1
- package/commands/design/flow.md +1 -1
- package/commands/design/ui.md +1 -1
- package/commands/measure/experiment.md +1 -1
- package/commands/measure/metrics.md +1 -1
- package/commands/measure/results.md +1 -1
- package/commands/plan/api.md +1 -1
- package/commands/plan/challenge.md +1 -1
- package/commands/plan/decide.md +1 -1
- package/commands/plan/feature.md +1 -1
- package/commands/plan/requirements.md +1 -1
- package/commands/remember/context.md +1 -1
- package/commands/remember/handoff.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/commands/review/code.md +1 -1
- package/commands/review/quality.md +1 -1
- package/commands/review/security.md +1 -1
- package/commands/ship/ready.md +1 -1
- package/commands/ship/release.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/understand/docs.md +1 -1
- package/commands/understand/this.md +1 -1
- package/commands/understand/why.md +1 -1
- package/commands/work/clean.md +1 -1
- package/commands/work/drive.md +1 -1
- package/commands/work/optimize-prompts.md +1 -1
- package/commands/work/parallel-review.md +1 -1
- package/lib/auto-docs.mjs +7 -7
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/comment-lint.mjs +134 -0
- package/lib/contracts/validate.mjs +323 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/docs-verify.mjs +59 -6
- package/lib/doctor/cli.mjs +16 -1
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/watchers/consistency.mjs +310 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +211 -1
- package/lib/embed/cli.mjs +124 -3
- package/lib/embed/daemon.mjs +43 -4
- package/lib/embed/docs-lifecycle.mjs +1 -1
- package/lib/embed/inbox.mjs +2 -0
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/mcp-audit.mjs +2 -1
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/init-unified.mjs +55 -65
- package/lib/intake/classify.mjs +108 -24
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +6 -1
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/prepare.mjs +0 -1
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +2 -0
- package/lib/mcp/tools/workflow.mjs +25 -0
- package/lib/mcp-catalog.json +12 -8
- package/lib/mcp-platform-config.mjs +16 -8
- package/lib/migrations/index.mjs +106 -0
- package/lib/migrations/v1-baseline.mjs +33 -0
- package/lib/observation-store.mjs +9 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/reflect.mjs +49 -12
- package/lib/server/index.mjs +22 -28
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/telemetry/client.mjs +5 -1
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/package.json +4 -1
- package/personas/construct.md +3 -1
- package/rules/common/development-workflow.md +2 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/review-before-change.md +58 -0
- package/scripts/sync-agents.mjs +45 -14
- package/templates/docs/construct_guide.md +6 -6
- package/templates/docs/skill-artifact.md +1 -1
package/lib/auto-docs.mjs
CHANGED
|
@@ -158,10 +158,10 @@ function buildAgentsTable(rootDir) {
|
|
|
158
158
|
const agents = registry.agents ?? registry;
|
|
159
159
|
if (!Array.isArray(agents)) return '';
|
|
160
160
|
|
|
161
|
-
const rows = ['|
|
|
161
|
+
const rows = ['| Specialist | Tier | Purpose |', '|---|---|---|'];
|
|
162
162
|
for (const agent of agents.slice(0, 30)) {
|
|
163
|
-
const name = agent.
|
|
164
|
-
const tier = agent.tier ?? agent.model_tier ?? 'n/a';
|
|
163
|
+
const name = agent.name ?? agent.id ?? 'n/a';
|
|
164
|
+
const tier = agent.modelTier ?? agent.tier ?? agent.model_tier ?? 'n/a';
|
|
165
165
|
// Strip em-dashes from descriptions so the prose lint stays clean across
|
|
166
166
|
// AUTO regen. Em-dashes are allowed in the registry's source descriptions
|
|
167
167
|
// but never in the rendered markdown.
|
|
@@ -195,7 +195,7 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
|
|
|
195
195
|
},
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
|
-
file: path.join(rootDir, 'docs', 'concepts', 'architecture.
|
|
198
|
+
file: path.join(rootDir, 'docs', 'concepts', 'architecture.mdx'),
|
|
199
199
|
regions: {
|
|
200
200
|
agents: buildAgentsTable(rootDir),
|
|
201
201
|
},
|
|
@@ -396,13 +396,13 @@ function renderAgentsPage(rootDir) {
|
|
|
396
396
|
if (!table) return null;
|
|
397
397
|
return [
|
|
398
398
|
'---',
|
|
399
|
-
'title:
|
|
400
|
-
'description: The 28
|
|
399
|
+
'title: Specialists',
|
|
400
|
+
'description: The 28 specialists behind the construct persona. Generated from agents/registry.json.',
|
|
401
401
|
'---',
|
|
402
402
|
'',
|
|
403
403
|
'> Generated from `agents/registry.json`. Re-run `construct docs:site` to refresh.',
|
|
404
404
|
'',
|
|
405
|
-
'Construct ships one persona (`construct`)
|
|
405
|
+
'Construct ships one persona (`construct`) and 28 specialists behind it. You address `@construct` for all everyday work; it routes to specialists internally. Each specialist has a role, model tier, and prompt file that defines its decision authority.',
|
|
406
406
|
'',
|
|
407
407
|
table,
|
|
408
408
|
'',
|
package/lib/boundary.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/boundary.mjs — Authenticated parent/child boundary registration.
|
|
3
|
+
*
|
|
4
|
+
* Verifies a parent Construct instance is reachable, validates an HMAC
|
|
5
|
+
* signature over `childInstanceId|nonce` when a shared secret is configured,
|
|
6
|
+
* and persists the binding to ~/.construct/boundary.json (mode 0600). A
|
|
7
|
+
* different parent rotates the registration only when explicitly allowed via
|
|
8
|
+
* CONSTRUCT_BOUNDARY_ALLOW_OVERRIDE=1; prior configs are archived alongside
|
|
9
|
+
* the active one. Exposed as a standalone module so the dashboard endpoint
|
|
10
|
+
* and the functional tests share the same logic.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto';
|
|
17
|
+
import { request as httpRequest } from 'node:http';
|
|
18
|
+
import { request as httpsRequest } from 'node:https';
|
|
19
|
+
|
|
20
|
+
export const BOUNDARY_VERSION = '1.0';
|
|
21
|
+
|
|
22
|
+
function boundaryDir(home = homedir()) {
|
|
23
|
+
return join(home, '.construct');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function boundaryConfigPath(home = homedir()) {
|
|
27
|
+
return join(boundaryDir(home), 'boundary.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function registerBoundary({
|
|
31
|
+
parentInstance,
|
|
32
|
+
parentUrl,
|
|
33
|
+
childInstanceId,
|
|
34
|
+
nonce,
|
|
35
|
+
signature,
|
|
36
|
+
sharedSecret = null,
|
|
37
|
+
home = homedir(),
|
|
38
|
+
allowOverride = process.env.CONSTRUCT_BOUNDARY_ALLOW_OVERRIDE === '1',
|
|
39
|
+
probe = probeParent,
|
|
40
|
+
}) {
|
|
41
|
+
if (!parentInstance || !parentUrl) {
|
|
42
|
+
return { ok: false, status: 400, error: 'parentInstance and parentUrl are required' };
|
|
43
|
+
}
|
|
44
|
+
if (!childInstanceId) {
|
|
45
|
+
return { ok: false, status: 400, error: 'childInstanceId is required' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (sharedSecret) {
|
|
49
|
+
if (!nonce || !signature) {
|
|
50
|
+
return { ok: false, status: 401, error: 'nonce and signature are required when a shared secret is configured' };
|
|
51
|
+
}
|
|
52
|
+
const expected = createHmac('sha256', sharedSecret).update(`${childInstanceId}|${nonce}`).digest('hex');
|
|
53
|
+
const a = Buffer.from(expected, 'hex');
|
|
54
|
+
let b;
|
|
55
|
+
try { b = Buffer.from(String(signature), 'hex'); } catch { return { ok: false, status: 401, error: 'invalid signature encoding' }; }
|
|
56
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
57
|
+
return { ok: false, status: 401, error: 'signature mismatch' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reachable = await probe(parentUrl);
|
|
62
|
+
if (!reachable.ok) {
|
|
63
|
+
return { ok: false, status: 502, error: `parent unreachable: ${reachable.error}` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dir = boundaryDir(home);
|
|
67
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
68
|
+
const cfgPath = boundaryConfigPath(home);
|
|
69
|
+
|
|
70
|
+
let prior = null;
|
|
71
|
+
if (existsSync(cfgPath)) {
|
|
72
|
+
try { prior = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch { prior = null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (prior && prior.parentInstance && prior.parentInstance !== parentInstance) {
|
|
76
|
+
if (!allowOverride) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
status: 409,
|
|
80
|
+
error: `child already bound to parent '${prior.parentInstance}'; set CONSTRUCT_BOUNDARY_ALLOW_OVERRIDE=1 to rotate`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const archivePath = join(dir, `boundary.${Date.now()}.json`);
|
|
84
|
+
try { writeFileSync(archivePath, JSON.stringify(prior, null, 2)); } catch { /* archive is best effort */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = {
|
|
88
|
+
parentInstance,
|
|
89
|
+
parentUrl,
|
|
90
|
+
childInstanceId,
|
|
91
|
+
registeredAt: new Date().toISOString(),
|
|
92
|
+
rotatedFrom: prior?.parentInstance || null,
|
|
93
|
+
nonce: nonce || randomBytes(16).toString('hex'),
|
|
94
|
+
boundaryVersion: BOUNDARY_VERSION,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
writeFileSync(cfgPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
98
|
+
return { ok: true, config, path: cfgPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function probeParent(parentUrl) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
let parsed;
|
|
104
|
+
try { parsed = new URL(parentUrl); } catch { return resolve({ ok: false, error: 'invalid URL' }); }
|
|
105
|
+
if (!/^https?:$/.test(parsed.protocol)) return resolve({ ok: false, error: 'unsupported protocol' });
|
|
106
|
+
const fn = parsed.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
107
|
+
const req = fn({
|
|
108
|
+
method: 'HEAD',
|
|
109
|
+
protocol: parsed.protocol,
|
|
110
|
+
hostname: parsed.hostname,
|
|
111
|
+
port: parsed.port,
|
|
112
|
+
path: parsed.pathname || '/',
|
|
113
|
+
timeout: 3000,
|
|
114
|
+
}, (response) => {
|
|
115
|
+
resolve({ ok: response.statusCode < 500 });
|
|
116
|
+
response.resume();
|
|
117
|
+
});
|
|
118
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
|
|
119
|
+
req.on('error', (err) => resolve({ ok: false, error: err.message }));
|
|
120
|
+
req.end();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function signBoundaryRequest({ childInstanceId, nonce, sharedSecret }) {
|
|
125
|
+
return createHmac('sha256', sharedSecret).update(`${childInstanceId}|${nonce}`).digest('hex');
|
|
126
|
+
}
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/cache-strategy-google.js — Google Gemini cache strategy.
|
|
2
|
+
* lib/cache-strategy-google.js — Google Gemini cache strategy (agnostic skeleton).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Gemini uses a resource-based cachedContent API rather than per-message
|
|
5
|
+
* annotations. Construct does not own that integration: this module exposes
|
|
6
|
+
* the same surface as the other cache-strategy adapters and resolves cached
|
|
7
|
+
* resources via a pluggable resolver. The default resolver returns null
|
|
8
|
+
* (no cached resource), which is the safe agnostic default — callers will
|
|
9
|
+
* fall through to a fresh request. Provider plugins or operators can register
|
|
10
|
+
* a resolver via `setCachedContentResolver` to enable resource caching.
|
|
7
11
|
*/
|
|
8
12
|
import { estimateTokens } from './token-engine.js';
|
|
9
13
|
|
|
10
14
|
const GEMINI_CACHE_ENDPOINT = 'https://generativelanguage.googleapis.com/v1';
|
|
11
15
|
|
|
16
|
+
let cachedContentResolver = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a resolver responsible for producing a cachedContent resource name
|
|
20
|
+
* for a given system prompt. Contract:
|
|
21
|
+
* resolver({ systemText, caps, apiKey, modelId }) -> Promise<string|null>
|
|
22
|
+
* Returning null means no cached resource is available; the annotation is
|
|
23
|
+
* omitted and the request proceeds without a cached prefix.
|
|
24
|
+
*/
|
|
25
|
+
export function setCachedContentResolver(resolver) {
|
|
26
|
+
cachedContentResolver = typeof resolver === 'function' ? resolver : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
|
-
* Annotate prompt structure for Google Gemini models.
|
|
14
|
-
*
|
|
30
|
+
* Annotate prompt structure for Google Gemini models. Returns the shared
|
|
31
|
+
* annotation shape that every cache-strategy adapter emits.
|
|
15
32
|
*
|
|
16
33
|
* @param {object} promptStructure - { system, messages }
|
|
17
34
|
* @param {object} caps - Provider capabilities
|
|
@@ -22,12 +39,10 @@ export async function annotate(promptStructure, caps, { apiKey, modelId } = {})
|
|
|
22
39
|
const { system, messages } = promptStructure || {};
|
|
23
40
|
|
|
24
41
|
let cachedContentName = null;
|
|
25
|
-
|
|
26
|
-
if (apiKey && system) {
|
|
42
|
+
if (cachedContentResolver && apiKey && system) {
|
|
27
43
|
try {
|
|
28
|
-
cachedContentName = await
|
|
29
|
-
} catch
|
|
30
|
-
}
|
|
44
|
+
cachedContentName = await cachedContentResolver({ systemText: system, caps, apiKey, modelId });
|
|
45
|
+
} catch { /* resolver failure is non-fatal — fall through with no annotation */ }
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
return {
|
|
@@ -49,26 +64,6 @@ export async function annotate(promptStructure, caps, { apiKey, modelId } = {})
|
|
|
49
64
|
};
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
async function createCachedContent(systemText, caps, apiKey, modelId) {
|
|
53
|
-
// POST to https://generativelanguage.googleapis.com/v1/cachedContents
|
|
54
|
-
// Body: { model: 'gemini-1.5-pro', contents: [{ parts: [{ text: systemText }] }] }
|
|
55
|
-
// Returns: { name: 'cachedContents/abc123' }
|
|
56
|
-
|
|
57
|
-
const model = extractModelName(modelId);
|
|
58
|
-
const url = `${GEMINI_CACHE_ENDPOINT}/cachedContents?key=${apiKey}`;
|
|
59
|
-
|
|
60
|
-
// This is async but we don't want to block dispatch
|
|
61
|
-
// In production, this should be pre-cached
|
|
62
|
-
// For now, return null (defer to Phase C follow-up with live probing)
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function extractModelName(modelId) {
|
|
67
|
-
const id = String(modelId || '').toLowerCase();
|
|
68
|
-
const match = id.match(/google\/(gemini-[^\/]+)/);
|
|
69
|
-
return match ? match[1] : 'gemini-1.5-pro';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
67
|
export async function capabilities() {
|
|
73
68
|
return {
|
|
74
69
|
supportsAnnotations: false,
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -29,10 +29,29 @@ const MD_HEADER_GLOBS = [
|
|
|
29
29
|
/^commands\//,
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
+
// Artifact paths receive an additional prose-lint pass for fabrication patterns
|
|
33
|
+
// (manufactured confidence, hand-wave percentages, mind-reading, speculation).
|
|
34
|
+
// Scope is deliberately narrow — READMEs, cookbook, concepts, and reference are
|
|
35
|
+
// unaffected. The patterns live in BANNED_ARTIFACT below; the severity (block
|
|
36
|
+
// vs warn) is controlled by CONSTRUCT_ARTIFACT_LINT_MODE.
|
|
37
|
+
const ARTIFACT_PATH_GLOBS = [
|
|
38
|
+
/^docs\/prd\//,
|
|
39
|
+
/^docs\/adr\//,
|
|
40
|
+
/^docs\/rfc\//,
|
|
41
|
+
/^docs\/research\//,
|
|
42
|
+
/^\.cx\/knowledge\//,
|
|
43
|
+
/^\.cx\/handoffs\//,
|
|
44
|
+
/^\.cx\/research\//,
|
|
45
|
+
];
|
|
46
|
+
|
|
32
47
|
function relPath(rootDir, absPath) {
|
|
33
48
|
return path.relative(rootDir, absPath).replace(/\\/g, '/');
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
function isArtifactPath(rel) {
|
|
52
|
+
return ARTIFACT_PATH_GLOBS.some((r) => r.test(rel));
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
function requiresHeader(rel) {
|
|
37
56
|
const ext = path.extname(rel);
|
|
38
57
|
if (['.yaml', '.yml', '.json', '.jsonl', '.toml'].includes(ext)) return { required: false, type: null };
|
|
@@ -88,6 +107,13 @@ const BANNED = [
|
|
|
88
107
|
// step markers — only flag in `//` comments. Ordered lists are
|
|
89
108
|
// conventional inside JSDoc decision-tree and flow documentation.
|
|
90
109
|
{ pattern: /\/\/\s+\d+\.\s+\w/, label: 'step marker: use function names or block structure instead of "// 1. Step"' },
|
|
110
|
+
// deferred-implementation wording: signals incomplete work that should
|
|
111
|
+
// either ship or be removed before release.
|
|
112
|
+
{ pattern: new RegExp(`${CMT}.*\\bphase\\s+[abc]\\s+follow-?up\\b`, 'i'), label: 'deferred implementation: "Phase X follow-up" — ship it or remove it' },
|
|
113
|
+
{ pattern: new RegExp(`${CMT}.*\\bin a real implementation\\b`, 'i'), label: 'deferred implementation: "in a real implementation" — implement the contract honestly' },
|
|
114
|
+
{ pattern: new RegExp(`${CMT}.*\\bwould go here\\b`, 'i'), label: 'deferred implementation: "would go here" — implement or delete the placeholder' },
|
|
115
|
+
{ pattern: new RegExp(`${CMT}.*\\bcoming soon\\b`, 'i'), label: 'deferred implementation: "coming soon" — not a release-ready statement' },
|
|
116
|
+
{ pattern: new RegExp(`${CMT}.*\\bnot yet supported\\b`, 'i'), label: 'deferred implementation: "not yet supported" — document the supported contract instead' },
|
|
91
117
|
// markdown equivalents
|
|
92
118
|
{ pattern: /<!--.*\b(?:added for|added recently|just added|new:)\b.*-->/i, label: 'point-in-time in markdown comment' },
|
|
93
119
|
{ pattern: /<!--.*\b(?:used by|called from|only consumer)\b.*-->/i, label: 'caller reference in markdown comment' },
|
|
@@ -102,16 +128,111 @@ const BANNED = [
|
|
|
102
128
|
const BANNED_CHECK_SKIP = [
|
|
103
129
|
'lib/comment-lint.mjs',
|
|
104
130
|
'rules/common/comments.md',
|
|
131
|
+
'rules/common/no-fabrication.md',
|
|
105
132
|
'tests/comment-lint.test.mjs',
|
|
106
133
|
'commands/work/clean.md',
|
|
107
134
|
'skills/utility/clean-code.md',
|
|
108
135
|
];
|
|
109
136
|
|
|
137
|
+
// Artifact-prose patterns. Scoped to ARTIFACT_PATH_GLOBS. Each entry can mark
|
|
138
|
+
// requireCitation: true — in that case the violation is suppressed when a
|
|
139
|
+
// citation (`[source: …]`, http URL, or footnote ref) appears in the same line
|
|
140
|
+
// or within 2 lines of context.
|
|
141
|
+
const BANNED_ARTIFACT = [
|
|
142
|
+
{
|
|
143
|
+
pattern: /\b(?:clearly|obviously|undoubtedly|definitely|certainly|surely)\b/i,
|
|
144
|
+
label: 'manufactured confidence: "clearly/obviously/undoubtedly/etc" smuggles certainty without evidence (no-fabrication §4)',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
pattern: /\b\d{1,3}\s*%/,
|
|
148
|
+
label: 'unattributed percentage: quantitative claims require a [source: …] citation (no-fabrication §4)',
|
|
149
|
+
requireCitation: true,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
pattern: /\b(?:users|customers|everyone)\s+(?:want|expect|prefer|agree|need)\b/i,
|
|
153
|
+
label: 'customer mind-reading: "users want / customers expect / everyone agrees" requires a [source: …] citation (no-fabrication §5)',
|
|
154
|
+
requireCitation: true,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: /\b(?:will\s+(?:likely|probably)|should\s+improve|expected\s+to\s+(?:improve|reduce|increase|drop|rise))\b/i,
|
|
158
|
+
label: 'speculative projection: future-tense claims need a baseline + projection source (no-fabrication §3, §4)',
|
|
159
|
+
requireCitation: true,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
110
163
|
function isBannedCheckSkipped(filePath) {
|
|
111
164
|
if (!filePath) return false;
|
|
112
165
|
return BANNED_CHECK_SKIP.some((suffix) => filePath.endsWith(suffix));
|
|
113
166
|
}
|
|
114
167
|
|
|
168
|
+
// Track fenced code blocks ( ``` … ``` ) and YAML frontmatter ( --- … --- )
|
|
169
|
+
// so artifact-prose patterns inside code or metadata are not flagged.
|
|
170
|
+
function buildSkipMap(lines) {
|
|
171
|
+
const skip = new Array(lines.length).fill(false);
|
|
172
|
+
let inFence = false;
|
|
173
|
+
let inFrontmatter = false;
|
|
174
|
+
let frontmatterClosed = false;
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
const line = lines[i];
|
|
177
|
+
if (i === 0 && line.trim() === '---') {
|
|
178
|
+
inFrontmatter = true;
|
|
179
|
+
skip[i] = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (inFrontmatter) {
|
|
183
|
+
skip[i] = true;
|
|
184
|
+
if (line.trim() === '---') {
|
|
185
|
+
inFrontmatter = false;
|
|
186
|
+
frontmatterClosed = true;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (/^```/.test(line)) {
|
|
191
|
+
inFence = !inFence;
|
|
192
|
+
skip[i] = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (inFence) {
|
|
196
|
+
skip[i] = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (/^\s{0,3}#{1,6}\s/.test(line)) skip[i] = true;
|
|
200
|
+
// Markdown table rows (start and end with `|`) are typically targets,
|
|
201
|
+
// specs, or reference lookups, not narrative prose. Skip to keep the
|
|
202
|
+
// pattern bank focused on the prose surface where embellishment lives.
|
|
203
|
+
if (/^\s*\|.*\|\s*$/.test(line)) skip[i] = true;
|
|
204
|
+
}
|
|
205
|
+
void frontmatterClosed;
|
|
206
|
+
return skip;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasCitationNearby(lines, idx) {
|
|
210
|
+
const lo = Math.max(0, idx - 2);
|
|
211
|
+
const hi = Math.min(lines.length, idx + 3);
|
|
212
|
+
for (let i = lo; i < hi; i++) {
|
|
213
|
+
if (/\[source:|\bhttps?:\/\/|\[\^\d+\]/i.test(lines[i])) return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function checkArtifactBanned(content, rel) {
|
|
219
|
+
if (!isArtifactPath(rel)) return [];
|
|
220
|
+
const lines = content.split('\n');
|
|
221
|
+
const skip = buildSkipMap(lines);
|
|
222
|
+
const violations = [];
|
|
223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
224
|
+
if (skip[i]) continue;
|
|
225
|
+
if (lines[i].includes('construct-lint-ignore')) continue;
|
|
226
|
+
for (const entry of BANNED_ARTIFACT) {
|
|
227
|
+
if (entry.pattern.test(lines[i])) {
|
|
228
|
+
if (entry.requireCitation && hasCitationNearby(lines, i)) continue;
|
|
229
|
+
violations.push({ line: i + 1, label: entry.label, kind: 'artifact' });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
|
|
115
236
|
function checkBanned(content, filePath) {
|
|
116
237
|
if (isBannedCheckSkipped(filePath)) return [];
|
|
117
238
|
|
|
@@ -180,6 +301,19 @@ export function lintFile(filePath, { rootDir = process.cwd(), fix = false } = {}
|
|
|
180
301
|
const banned = checkBanned(content, filePath);
|
|
181
302
|
for (const w of banned) warnings.push(w);
|
|
182
303
|
|
|
304
|
+
// Artifact-prose lint applies to markdown files under intake-fed artifact
|
|
305
|
+
// paths (PRD, ADR, RFC, research, knowledge, handoffs). Severity is governed
|
|
306
|
+
// by CONSTRUCT_ARTIFACT_LINT_MODE: 'block' (CLI / release-gate) routes hits
|
|
307
|
+
// to errors[]; 'warn' (PostToolUse hook default) routes to warnings[] with
|
|
308
|
+
// kind:'artifact' so the hook can let the edit through while still surfacing
|
|
309
|
+
// the finding.
|
|
310
|
+
const artifactHits = checkArtifactBanned(content, rel);
|
|
311
|
+
if (artifactHits.length > 0) {
|
|
312
|
+
const mode = process.env.CONSTRUCT_ARTIFACT_LINT_MODE || 'warn';
|
|
313
|
+
const bucket = mode === 'block' ? errors : warnings;
|
|
314
|
+
for (const v of artifactHits) bucket.push(v);
|
|
315
|
+
}
|
|
316
|
+
|
|
183
317
|
return { path: rel, errors, warnings };
|
|
184
318
|
}
|
|
185
319
|
|