@geraldmaron/construct 1.0.18 → 1.0.19
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 -4
- package/bin/construct +26 -3
- package/db/schema/003_observation_reconciliation.sql +14 -0
- package/lib/bootstrap/resources.mjs +0 -1
- package/lib/cli-commands.mjs +33 -5
- package/lib/comment-lint.mjs +44 -0
- package/lib/contracts/validate.mjs +106 -0
- package/lib/decisions/enforced-baseline.json +23 -0
- package/lib/decisions/golden.mjs +87 -0
- package/lib/decisions/precedence.mjs +46 -0
- package/lib/decisions/registry.mjs +469 -0
- package/lib/deployment/parity-contract.mjs +148 -0
- package/lib/embed/cli.mjs +11 -0
- package/lib/embed/conflict-detection.mjs +4 -4
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/reconcile.mjs +60 -0
- package/lib/gates-audit.mjs +2 -2
- package/lib/hooks/config-protection.mjs +22 -3
- package/lib/hooks/guard-bash.mjs +1 -1
- package/lib/init-docs.mjs +1 -0
- package/lib/mode-commands.mjs +6 -8
- package/lib/observation-store.mjs +16 -2
- package/lib/opencode-telemetry.mjs +1 -1
- package/lib/roles/cli.mjs +10 -2
- package/lib/roles/gateway.mjs +50 -1
- package/lib/scheduler/index.mjs +31 -0
- package/lib/server/index.mjs +13 -3
- package/lib/server/static/index.html +1 -1
- package/lib/setup.mjs +6 -0
- package/lib/storage/hybrid-query.mjs +49 -38
- package/lib/storage/rrf.mjs +42 -0
- package/lib/storage/vector-client.mjs +18 -3
- package/lib/telemetry/backends/local.mjs +1 -1
- package/lib/telemetry/skill-calls.mjs +1 -1
- package/lib/templates/visual-requirements.mjs +84 -0
- package/package.json +9 -1
- package/rules/common/comments.md +3 -0
- package/rules/common/no-fabrication.md +3 -0
- package/rules/common/precedence.md +19 -0
- package/rules/common/research-sources.md +32 -0
- package/rules/common/research.md +59 -2
- package/skills/roles/data-engineer.pipeline.md +13 -1
- package/skills/roles/debugger.md +9 -0
- package/skills/roles/designer.accessibility.md +13 -3
- package/skills/roles/designer.md +10 -0
- package/skills/roles/engineer.platform.md +8 -0
- package/skills/roles/operator.md +10 -1
- package/skills/roles/operator.release.md +8 -0
- package/skills/roles/operator.sre.md +10 -1
- package/skills/roles/orchestrator.md +9 -2
- package/skills/roles/product-manager.business-strategy.md +10 -1
- package/skills/roles/researcher.explorer.md +12 -1
- package/skills/roles/researcher.ux.md +13 -1
- package/skills/roles/reviewer.devil-advocate.md +14 -2
- package/skills/roles/reviewer.evaluator.md +17 -4
- package/skills/roles/reviewer.trace.md +12 -1
- package/skills/roles/security.legal-compliance.md +8 -0
- package/skills/roles/security.md +11 -0
- package/specialists/contracts.json +18 -0
- package/specialists/prompts/cx-researcher.md +4 -2
- package/templates/docs/backlog-proposal.md +1 -1
- package/templates/docs/customer-profile.md +1 -1
- package/templates/docs/evidence-brief.md +5 -1
- package/templates/docs/incident-report.md +37 -21
- package/templates/docs/prfaq.md +2 -2
- package/templates/docs/product-intelligence-report.md +1 -1
- package/templates/docs/research-brief.md +8 -6
- package/templates/docs/research-finding.md +32 -7
- package/templates/docs/rfc.md +13 -1
- package/templates/docs/runbook.md +20 -1
- package/templates/docs/signal-brief.md +4 -1
- package/templates/docs/skill-artifact.md +27 -7
- package/templates/docs/strategy.md +23 -2
- package/lib/bootstrap/lazy-install.mjs +0 -161
- package/lib/embed/jobs/vector-sync.mjs +0 -198
- package/lib/knowledge/postgres-search.mjs +0 -132
- package/lib/services/pattern-promotion-service.mjs +0 -167
- package/lib/storage/unified-storage.mjs +0 -550
|
@@ -6,8 +6,24 @@ import { loadStateSnapshot, summarizeStateSnapshot } from './state-source.mjs';
|
|
|
6
6
|
import { describeSqlStore } from './sql-store.mjs';
|
|
7
7
|
import { describeVectorStore, searchLocalVectorIndex, vectorSearchLocal } from './vector-store.mjs';
|
|
8
8
|
import { createSqlClient, closeSqlClient, readVectorConfig } from './backend.mjs';
|
|
9
|
-
import { scoreByEmbedding } from './embeddings.mjs';
|
|
10
9
|
import { embedText, getEmbeddingModelInfo } from './embeddings-engine.mjs';
|
|
10
|
+
import { floatArrayToPgVector } from './vector-client.mjs';
|
|
11
|
+
import { reciprocalRankFusion } from './rrf.mjs';
|
|
12
|
+
|
|
13
|
+
// iterative_scan (pgvector >= 0.8.0) keeps a filtered ANN query from
|
|
14
|
+
// under-returning; harmless for the current unfiltered query, and the correct
|
|
15
|
+
// default once tag/metadata filters are added to the vector search.
|
|
16
|
+
|
|
17
|
+
async function supportsIterativeScan(client) {
|
|
18
|
+
try {
|
|
19
|
+
const [row] = await client`SELECT extversion AS v FROM pg_extension WHERE extname = 'vector'`;
|
|
20
|
+
if (!row?.v) return false;
|
|
21
|
+
const [major, minor] = String(row.v).split('.').map((n) => parseInt(n, 10));
|
|
22
|
+
return major > 0 || (major === 0 && minor >= 8);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
|
|
12
28
|
function collectFileCandidates(snapshot) {
|
|
13
29
|
const docs = [];
|
|
@@ -104,33 +120,38 @@ export function buildHybridSearchResults(rootDir, query, { limit = 10, env = pro
|
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10, env = process.env } = {}) {
|
|
123
|
+
export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10, env = process.env, embed = embedText, embeddingModel: modelOverride = null } = {}) {
|
|
108
124
|
const base = buildHybridSearchResults(rootDir, query, { limit, env });
|
|
109
125
|
const client = createSqlClient(env);
|
|
110
126
|
if (!client) return base;
|
|
111
127
|
|
|
112
128
|
// Resolve the active embedding model so the SQL filter and the query
|
|
113
129
|
// embedding agree on dimensionality and identity.
|
|
114
|
-
const
|
|
115
|
-
const embeddingModel = modelInfo.model;
|
|
130
|
+
const embeddingModel = modelOverride || (await getEmbeddingModelInfo({ env })).model;
|
|
116
131
|
|
|
117
132
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
133
|
+
const queryVec = floatArrayToPgVector((await embed(query, { env })).embedding);
|
|
134
|
+
const iterative = await supportsIterativeScan(client);
|
|
135
|
+
|
|
136
|
+
// Native pgvector ANN over the HNSW index (not a JS full scan). When the
|
|
137
|
+
// extension supports it, relaxed_order iterative scan runs inside the txn.
|
|
138
|
+
const runVector = (sql) => sql`
|
|
139
|
+
select d.id, d.kind, d.title, d.summary, d.source_path
|
|
120
140
|
from construct_documents d
|
|
121
141
|
join construct_embeddings e on e.document_id = d.id
|
|
122
142
|
where d.project = 'construct' and e.model = ${embeddingModel}
|
|
143
|
+
order by e.embedding <=> ${queryVec}
|
|
144
|
+
limit ${limit}
|
|
123
145
|
`;
|
|
146
|
+
const vectorHits = iterative
|
|
147
|
+
? await client.begin(async (sql) => {
|
|
148
|
+
await sql`SET LOCAL hnsw.iterative_scan = relaxed_order`;
|
|
149
|
+
return runVector(sql);
|
|
150
|
+
})
|
|
151
|
+
: await runVector(client);
|
|
124
152
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
embeddingRows.map((row) => ({ ...row, embedding: row.embedding })),
|
|
128
|
-
Array.from(queryEmbedding),
|
|
129
|
-
{ limit },
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const sqlHits = await client`
|
|
133
|
-
select id, kind, title, summary, body, source_path
|
|
153
|
+
const keywordHits = await client`
|
|
154
|
+
select id, kind, title, summary, source_path
|
|
134
155
|
from construct_documents
|
|
135
156
|
where project = 'construct'
|
|
136
157
|
and (title ilike ${`%${query}%`} or coalesce(summary, '') ilike ${`%${query}%`} or body ilike ${`%${query}%`})
|
|
@@ -138,39 +159,29 @@ export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10
|
|
|
138
159
|
limit ${limit}
|
|
139
160
|
`;
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
summary: hit.summary,
|
|
149
|
-
score: hit.score,
|
|
150
|
-
source_path: hit.source_path,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
for (const hit of sqlHits) {
|
|
154
|
-
if (merged.some((entry) => entry.id === hit.id)) continue;
|
|
155
|
-
merged.push({
|
|
156
|
-
id: hit.id,
|
|
157
|
-
kind: hit.kind,
|
|
158
|
-
title: hit.title,
|
|
159
|
-
summary: hit.summary,
|
|
160
|
-
score: 1,
|
|
161
|
-
source_path: hit.source_path,
|
|
162
|
-
});
|
|
162
|
+
// One consolidated ranking: file BM25 (base) + neural ANN + keyword, fused
|
|
163
|
+
// by Reciprocal Rank Fusion so three incompatible score scales merge by
|
|
164
|
+
// rank rather than magnitude.
|
|
165
|
+
const lists = [base.results, vectorHits, keywordHits];
|
|
166
|
+
const byId = new Map();
|
|
167
|
+
for (const list of lists) {
|
|
168
|
+
for (const it of list) if (it && !byId.has(it.id)) byId.set(it.id, it);
|
|
163
169
|
}
|
|
170
|
+
const results = reciprocalRankFusion(lists, { idOf: (x) => x.id, limit }).map(({ id, score }) => {
|
|
171
|
+
const it = byId.get(id);
|
|
172
|
+
return { id, kind: it.kind, title: it.title, summary: it.summary, score, source_path: it.source_path ?? null };
|
|
173
|
+
});
|
|
164
174
|
|
|
165
175
|
return {
|
|
166
176
|
...base,
|
|
167
|
-
results
|
|
177
|
+
results,
|
|
168
178
|
stores: {
|
|
169
179
|
...base.stores,
|
|
170
180
|
vector: {
|
|
171
181
|
...base.stores.vector,
|
|
172
182
|
...readVectorConfig(env),
|
|
173
183
|
model: embeddingModel,
|
|
184
|
+
iterativeScan: iterative,
|
|
174
185
|
},
|
|
175
186
|
sql: {
|
|
176
187
|
...base.stores.sql,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/storage/rrf.mjs — Reciprocal Rank Fusion for hybrid retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Combines N independently-ranked result lists (e.g. BM25 keyword and cosine
|
|
5
|
+
* vector) into one ranking by summing 1/(k + rank) across the lists a document
|
|
6
|
+
* appears in. Fusion is by RANK, not raw score, so it merges lists whose scores
|
|
7
|
+
* live on incompatible scales — BM25 is unbounded-positive, cosine is [-1,1] —
|
|
8
|
+
* without any normalization or hand-tuned weighting. k smooths how much top
|
|
9
|
+
* ranks dominate; 60 is the field default (Cormack, Clarke & Büttcher,
|
|
10
|
+
* "Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning
|
|
11
|
+
* Methods", SIGIR 2009) and the dominant fusion method across search engines.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Each ranked list is ordered best-first. A document's RRF score is the sum,
|
|
15
|
+
// over every list it appears in, of 1/(k + rank) where rank is 1-based.
|
|
16
|
+
|
|
17
|
+
export function reciprocalRankFusion(rankedLists, { k = 60, idOf = (x) => x.id, limit = null } = {}) {
|
|
18
|
+
if (!Array.isArray(rankedLists) || rankedLists.length === 0) return [];
|
|
19
|
+
if (!Number.isFinite(k) || k <= 0) throw new Error('reciprocalRankFusion: k must be a positive number');
|
|
20
|
+
|
|
21
|
+
const scores = new Map();
|
|
22
|
+
const items = new Map();
|
|
23
|
+
|
|
24
|
+
for (const list of rankedLists) {
|
|
25
|
+
if (!Array.isArray(list)) continue;
|
|
26
|
+
for (let rank = 1; rank <= list.length; rank += 1) {
|
|
27
|
+
const item = list[rank - 1];
|
|
28
|
+
if (item == null) continue;
|
|
29
|
+
const id = idOf(item);
|
|
30
|
+
if (id == null) continue;
|
|
31
|
+
scores.set(id, (scores.get(id) || 0) + 1 / (k + rank));
|
|
32
|
+
if (!items.has(id)) items.set(id, item);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Deterministic order: by fused score desc, then id asc to break ties stably.
|
|
37
|
+
const fused = [...scores.entries()]
|
|
38
|
+
.sort((a, b) => (b[1] - a[1]) || String(a[0]).localeCompare(String(b[0])))
|
|
39
|
+
.map(([id, score]) => ({ item: items.get(id), id, score }));
|
|
40
|
+
|
|
41
|
+
return limit != null ? fused.slice(0, limit) : fused;
|
|
42
|
+
}
|
|
@@ -161,7 +161,7 @@ export class VectorClient {
|
|
|
161
161
|
/**
|
|
162
162
|
* Store an observation with its embedding.
|
|
163
163
|
*/
|
|
164
|
-
async storeObservation({ id, project, role, category, summary, content, tags, confidence, source, embedding, gitSha }) {
|
|
164
|
+
async storeObservation({ id, project, role, category, summary, content, tags, confidence, source, embedding, gitSha, contentHash = null, model = null }) {
|
|
165
165
|
const sql = await this._getSql();
|
|
166
166
|
if (!sql) return { mode: 'file', reason: 'no_sql' };
|
|
167
167
|
const expected = await this.getEngineDimensions();
|
|
@@ -169,8 +169,8 @@ export class VectorClient {
|
|
|
169
169
|
|
|
170
170
|
const embeddingVec = floatArrayToPgVector(embedding);
|
|
171
171
|
await sql`
|
|
172
|
-
INSERT INTO construct_observations (id, project, role, category, summary, content, tags, confidence, source, git_sha, embedding)
|
|
173
|
-
VALUES (${id}, ${project}, ${role}, ${category}, ${summary}, ${content}, ${JSON.stringify(tags || [])}, ${confidence || 0.8}, ${source || null}, ${gitSha || null}, ${embeddingVec})
|
|
172
|
+
INSERT INTO construct_observations (id, project, role, category, summary, content, tags, confidence, source, git_sha, embedding, content_hash, model)
|
|
173
|
+
VALUES (${id}, ${project}, ${role}, ${category}, ${summary}, ${content}, ${JSON.stringify(tags || [])}, ${confidence || 0.8}, ${source || null}, ${gitSha || null}, ${embeddingVec}, ${contentHash}, ${model})
|
|
174
174
|
ON CONFLICT (id) DO UPDATE SET
|
|
175
175
|
summary = EXCLUDED.summary,
|
|
176
176
|
content = EXCLUDED.content,
|
|
@@ -179,11 +179,26 @@ export class VectorClient {
|
|
|
179
179
|
source = EXCLUDED.source,
|
|
180
180
|
git_sha = EXCLUDED.git_sha,
|
|
181
181
|
embedding = EXCLUDED.embedding,
|
|
182
|
+
content_hash = EXCLUDED.content_hash,
|
|
183
|
+
model = EXCLUDED.model,
|
|
182
184
|
updated_at = now()
|
|
183
185
|
`;
|
|
184
186
|
return { mode: 'sql', id };
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
// Fingerprint = the (content_hash, model) a stored observation was embedded
|
|
190
|
+
// with. Reconciliation compares this against the live content + current model
|
|
191
|
+
// to find rows needing re-embedding without re-reading every vector.
|
|
192
|
+
|
|
193
|
+
async getObservationFingerprints(ids = []) {
|
|
194
|
+
const sql = await this._getSql();
|
|
195
|
+
if (!sql || !Array.isArray(ids) || ids.length === 0) return new Map();
|
|
196
|
+
const rows = await sql`
|
|
197
|
+
SELECT id, content_hash, model FROM construct_observations WHERE id = ANY(${ids})
|
|
198
|
+
`;
|
|
199
|
+
return new Map(rows.map((r) => [r.id, { contentHash: r.content_hash, model: r.model }]));
|
|
200
|
+
}
|
|
201
|
+
|
|
187
202
|
/**
|
|
188
203
|
* Search observations by vector similarity.
|
|
189
204
|
*/
|
|
@@ -30,7 +30,7 @@ export async function listTraces(teamId, windowMs) {
|
|
|
30
30
|
if (!existsSync(dir)) return [];
|
|
31
31
|
|
|
32
32
|
const since = Date.now() - windowMs;
|
|
33
|
-
const files = readdirSync(
|
|
33
|
+
const files = readdirSync(dir)
|
|
34
34
|
.filter((f) => f.endsWith('.jsonl'))
|
|
35
35
|
.sort()
|
|
36
36
|
.reverse()
|
|
@@ -32,7 +32,7 @@ export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'skill-calls.json
|
|
|
32
32
|
*
|
|
33
33
|
* @param {object} event
|
|
34
34
|
* @param {string} event.skillId — path-relative-to-skills/ without the .md, e.g. "roles/engineer.platform"
|
|
35
|
-
* @param {'mcp'|'prompt-composer'|'role-preload'|'
|
|
35
|
+
* @param {'mcp'|'prompt-composer'|'role-preload'|'validation'|'other'} event.source
|
|
36
36
|
* @param {string} [event.callerContext] — optional free-form context (agent name, MCP client id, etc.)
|
|
37
37
|
* @param {object} [opts]
|
|
38
38
|
* @param {string} [opts.logPath] — override the default log path (tests pass a tmpdir)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/templates/visual-requirements.mjs — required visuals per document type.
|
|
3
|
+
*
|
|
4
|
+
* The machine-readable form of docs/concepts/doc-visual-matrix.md: which doc
|
|
5
|
+
* types must carry which visual (a diagram of a given kind, or a table with given
|
|
6
|
+
* columns), expressed as postcondition checks so the existing validate.mjs engine
|
|
7
|
+
* enforces them. The shipped template for each listed type must satisfy its own
|
|
8
|
+
* requirements — pinned by tests/template-visuals.test.mjs — so the templates can
|
|
9
|
+
* never drift out of step with the matrix (beads wvbf.10 / wvbf.11).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { validateArtifactPostconditions } from '../contracts/validate.mjs';
|
|
13
|
+
|
|
14
|
+
export const VISUAL_REQUIREMENTS = {
|
|
15
|
+
runbook: [
|
|
16
|
+
{ id: 'runbook-diagnostic-flowchart', check: 'artifact-has-mermaid', diagram: 'flowchart' },
|
|
17
|
+
],
|
|
18
|
+
'incident-report': [
|
|
19
|
+
{ id: 'incident-timeline-table', check: 'artifact-table-has-columns', columns: ['Time (UTC)', 'Event'] },
|
|
20
|
+
],
|
|
21
|
+
rfc: [
|
|
22
|
+
{ id: 'rfc-sequence-diagram', check: 'artifact-has-mermaid', diagram: 'sequenceDiagram' },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function visualRequirementTypes() {
|
|
27
|
+
return Object.keys(VISUAL_REQUIREMENTS);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The structural floor of the quality rubric (ADR-0018): the sections a domain
|
|
31
|
+
// expert expects for each doc type. Each shipped template must satisfy its own
|
|
32
|
+
// entry — pinned by tests/structure-requirements.test.mjs — so a template can
|
|
33
|
+
// never quietly drop a required section. Section requirements compose with the
|
|
34
|
+
// VISUAL_REQUIREMENTS above; lintDocStructure runs both.
|
|
35
|
+
|
|
36
|
+
export const STRUCTURE_REQUIREMENTS = {
|
|
37
|
+
adr: ['Problem', 'Decision', 'Rejected alternatives', 'Consequences', 'Reversibility'],
|
|
38
|
+
rfc: ['Summary', 'Motivation', 'Proposed design', 'Risks', 'Verification'],
|
|
39
|
+
prd: ['Problem', 'Goals', 'Success metrics', 'Risks and mitigations'],
|
|
40
|
+
'research-brief': ['Sources', 'Findings', 'Confidence summary', 'Recommendation'],
|
|
41
|
+
'incident-report': ['Summary', 'Severity rationale', 'Impact', 'Timeline', 'Trigger', 'Root cause', 'Contributing factors', 'Action items'],
|
|
42
|
+
runbook: ['Alert trigger', 'Symptoms', 'Impact', 'Severity and response', 'Diagnostic steps', 'Remediation', 'Rollback', 'Escalation'],
|
|
43
|
+
strategy: ['Vision', 'Bets', 'Non-bets', 'North Star Metric', 'Metrics', 'Milestones', 'Risks'],
|
|
44
|
+
'signal-brief': ['Signal', 'Evidence', 'Counter-signal', 'What would make this actionable'],
|
|
45
|
+
prfaq: ['Problem statement', 'Press release', 'External FAQ', 'Internal FAQ', 'Evidence appendix'],
|
|
46
|
+
'customer-profile': ['Snapshot', 'Active pain points', 'Open asks', 'Evidence links'],
|
|
47
|
+
'one-pager': ['Problem', 'Proposal', 'Why now', 'Success measure', 'Cost', 'Asks'],
|
|
48
|
+
'product-intelligence-report': ['Executive readout', 'Evidence base', 'Themes', 'Customer asks', 'Product implications', 'Recommended actions', 'Gaps and risks'],
|
|
49
|
+
'backlog-proposal': ['Source evidence', 'Proposed changes', 'Duplicate and conflict check', 'Approval request'],
|
|
50
|
+
'persona-artifact': ['Goals', 'Frustrations', 'Decision rights', 'Output contract', 'Failure modes', 'Evidence'],
|
|
51
|
+
'skill-artifact': ['What this skill produces', 'When to invoke it', 'Competency rubric', 'Failure modes', 'Worked example'],
|
|
52
|
+
'research-finding': ['SOURCES', 'FINDINGS', 'INFERENCES', 'CONFIDENCE', 'GAPS', 'RECOMMENDATION'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function structureRequirementTypes() {
|
|
56
|
+
return [...new Set([...Object.keys(STRUCTURE_REQUIREMENTS), ...Object.keys(VISUAL_REQUIREMENTS)])];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lint one document against both the required sections and required visuals for
|
|
61
|
+
* its type. Returns an array of violation strings (empty when satisfied or when
|
|
62
|
+
* the type declares no requirements).
|
|
63
|
+
*/
|
|
64
|
+
export function lintDocStructure(filePath, type) {
|
|
65
|
+
const sectionChecks = (STRUCTURE_REQUIREMENTS[type] || []).map((section) => ({
|
|
66
|
+
id: `${type}-section-${section}`,
|
|
67
|
+
check: 'artifact-has-section',
|
|
68
|
+
section,
|
|
69
|
+
}));
|
|
70
|
+
const postconditions = [...sectionChecks, ...(VISUAL_REQUIREMENTS[type] || [])];
|
|
71
|
+
if (postconditions.length === 0) return [];
|
|
72
|
+
return validateArtifactPostconditions({ contract: { postconditions }, artifactPath: filePath });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Lint one document against the visual requirements for its type. Returns an
|
|
77
|
+
* array of violation strings (empty when satisfied, or when the type has no
|
|
78
|
+
* declared visual requirements).
|
|
79
|
+
*/
|
|
80
|
+
export function lintDocVisuals(filePath, type) {
|
|
81
|
+
const postconditions = VISUAL_REQUIREMENTS[type];
|
|
82
|
+
if (!postconditions) return [];
|
|
83
|
+
return validateArtifactPostconditions({ contract: { postconditions }, artifactPath: filePath });
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geraldmaron/construct",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
|
|
6
6
|
"bin": {
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"test": "node scripts/run-tests.mjs",
|
|
55
55
|
"test:functional": "node --test --test-timeout=120000 --test-concurrency=1 tests/functional/*.functional.test.mjs",
|
|
56
56
|
"test:unit": "node scripts/run-tests.mjs --exclude=tests/functional",
|
|
57
|
+
"lint:js": "eslint bin/construct \"lib/**/*.mjs\" \"scripts/**/*.mjs\" \"tests/**/*.mjs\"",
|
|
58
|
+
"coverage": "c8 --reporter=text-summary --reporter=lcov --src=lib --src=bin node scripts/run-tests.mjs --exclude=tests/functional",
|
|
57
59
|
"docs:init": "node lib/init-docs.mjs --yes",
|
|
58
60
|
"docs:update": "node ./bin/construct docs:update",
|
|
59
61
|
"docs:site": "node ./bin/construct docs:site",
|
|
@@ -92,5 +94,11 @@
|
|
|
92
94
|
},
|
|
93
95
|
"overrides": {
|
|
94
96
|
"express-rate-limit": "8.5.1"
|
|
97
|
+
},
|
|
98
|
+
"devDependencies": {
|
|
99
|
+
"@eslint/js": "^9.39.4",
|
|
100
|
+
"c8": "^11.0.0",
|
|
101
|
+
"eslint": "^9.39.4",
|
|
102
|
+
"globals": "^17.6.0"
|
|
95
103
|
}
|
|
96
104
|
}
|
package/rules/common/comments.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Canonical precedence order for resolving conflicting guidance.
|
|
3
|
+
enforced_by: lib/decisions/precedence.mjs
|
|
4
|
+
adr_reference: ADR-0015
|
|
5
|
+
---
|
|
6
|
+
# Rule precedence
|
|
7
|
+
|
|
8
|
+
When two rules give contradictory direction for the same situation, the conflict resolves by tier, not by recency or proximity in the prompt. The canonical order, highest priority first:
|
|
9
|
+
|
|
10
|
+
1. **safety** — preventing destructive or irreversible harm (data loss, secret exposure, production damage).
|
|
11
|
+
2. **security** — preventing unauthorized access, injection, or privilege escalation.
|
|
12
|
+
3. **correctness** — producing truthful, accurate, non-fabricated output that does what it claims.
|
|
13
|
+
4. **durability** — keeping decisions and state from silently drifting or being lost.
|
|
14
|
+
5. **performance** — speed, cost, and resource efficiency.
|
|
15
|
+
6. **style** — naming, formatting, comment convention, and other presentation choices.
|
|
16
|
+
|
|
17
|
+
A rule may declare its tier in frontmatter (`precedence_tier: <tier>`). A higher tier always governs: a style rule never overrides a correctness rule, and a performance optimization never overrides a safety constraint. The resolver lives in `lib/decisions/precedence.mjs`; `construct decisions check` fails if a rule declares a tier outside this list.
|
|
18
|
+
|
|
19
|
+
This sets the resolution order. It does not detect whether two rules contradict — that judgment stays with the author and reviewer.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Per-domain community starting points for sentiment and signal research.
|
|
3
|
+
enforced_by: rules/common/research.md
|
|
4
|
+
adr_reference: ADR-0017
|
|
5
|
+
---
|
|
6
|
+
# Community source catalog
|
|
7
|
+
|
|
8
|
+
Starting points for **community signal** — sentiment, demand, friction, adoption experience — organized by research domain. These complement, never replace, the authoritative starting points in [research.md §2](research.md). Community sources are admissible only under the §10 checklist, and only for sentiment/experience claims (a source's class is relative to the claim — see research.md §2).
|
|
9
|
+
|
|
10
|
+
Treat every entry as a starting point, not a settled citation: confirm the venue is still active and record the post date and Admiralty grade (research.md §4, §10) before any community source becomes load-bearing.
|
|
11
|
+
|
|
12
|
+
| Domain | Reddit | Other community venues |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| AI tools, LLMs, agents | r/LocalLLaMA, r/MachineLearning, r/LanguageTechnology | Hacker News (Show HN / Ask HN), arXiv-sanity discussions, vendor Discords |
|
|
15
|
+
| Developer tools, IDEs, languages | r/programming, r/webdev, r/javascript, r/Python, r/rust, r/golang | Stack Overflow (by tag) + the annual Developer Survey, Hacker News, Lobsters |
|
|
16
|
+
| DevOps, platform, reliability | r/devops, r/sre, r/kubernetes, r/Terraform | CNCF Slack, Hacker News, platform vendor Discords |
|
|
17
|
+
| Security, vulnerabilities | r/netsec, r/cybersecurity, r/AskNetsec | HackerOne / Bugcrowd public disclosures, OSS-Security mailing list, Hacker News |
|
|
18
|
+
| Cloud infra, APIs, SDKs | r/aws, r/AZURE, r/googlecloud | Vendor community forums, provider Discords, Stack Overflow tags |
|
|
19
|
+
| Data / ML engineering | r/dataengineering, r/MachineLearning, r/datascience | dbt Community Slack, Hacker News |
|
|
20
|
+
| Product, market, adoption | r/SaaS, r/ProductManagement, r/startups | Hacker News (launches), Product Hunt discussion |
|
|
21
|
+
| Regulatory, compliance, privacy | r/privacy, r/gdpr | IAPP community forums (primary regulation text remains the authority) |
|
|
22
|
+
|
|
23
|
+
## How to read community signal
|
|
24
|
+
|
|
25
|
+
- **Corroboration over volume from one place.** The same pain point raised independently across multiple threads or subreddits is stronger than one viral post.
|
|
26
|
+
- **Engagement is evidence of resonance, not of truth.** High upvotes mean many people relate to the sentiment; they do not make a factual claim in the post true.
|
|
27
|
+
- **Recency matters most for fast-moving domains** (research.md §1) — a frustration from two years ago may already be resolved.
|
|
28
|
+
- **Record the grade.** Community sentiment sources are typically `D`–`F` on reliability; they reach `1`–`2` on credibility only when cross-corroborated. Do not inflate.
|
|
29
|
+
|
|
30
|
+
## References
|
|
31
|
+
|
|
32
|
+
- [Reddit](https://www.reddit.com), [Hacker News](https://news.ycombinator.com), [Stack Overflow Developer Survey](https://survey.stackoverflow.co)
|
package/rules/common/research.md
CHANGED
|
@@ -29,6 +29,17 @@ Use the narrowest, most authoritative starting point for the research domain:
|
|
|
29
29
|
|
|
30
30
|
Tertiary sources (blogs, forums, Q&A, AI-generated summaries) may help locate primaries. They are not sufficient evidence for load-bearing claims.
|
|
31
31
|
|
|
32
|
+
For where to look for community signal by domain, see [research-sources.md](research-sources.md).
|
|
33
|
+
|
|
34
|
+
### Source class is relative to the claim
|
|
35
|
+
|
|
36
|
+
A source's class is not fixed — it depends on what the claim is about. The same artifact can be primary for one claim and tertiary for another.
|
|
37
|
+
|
|
38
|
+
- A Reddit thread is **tertiary** for "what does API X do" (the spec is primary), but **primary** for "developers report friction with API X's DX" — a first-hand account is primary evidence of the attitude it expresses.
|
|
39
|
+
- A vendor blog post is **secondary** for a feature's behavior (the docs/source are primary), but **primary** for "the vendor publicly committed to X on date Y."
|
|
40
|
+
|
|
41
|
+
Classify by the claim. For sentiment, demand, adoption-experience, and friction claims, community and forum content is admissible primary evidence under the conditions in §10. For factual, version, security, pricing, and compatibility claims, community content stays tertiary — locate the primary.
|
|
42
|
+
|
|
32
43
|
## 3. Start order
|
|
33
44
|
|
|
34
45
|
Start with the narrowest authoritative source that can answer the question:
|
|
@@ -52,7 +63,8 @@ Start with the narrowest authoritative source that can answer the question:
|
|
|
52
63
|
Record:
|
|
53
64
|
|
|
54
65
|
- source title or path
|
|
55
|
-
- source class: internal, primary, secondary, or tertiary
|
|
66
|
+
- source class: internal, primary, secondary, or tertiary (for the specific claim — see §2)
|
|
67
|
+
- Admiralty grade: source reliability `A`–`F` and information credibility `1`–`6` (see §10), recorded together, e.g. `B2`
|
|
56
68
|
- version or revision when applicable
|
|
57
69
|
- publication date, release date, or access date
|
|
58
70
|
- why this source is relevant
|
|
@@ -119,7 +131,52 @@ Research outputs should include:
|
|
|
119
131
|
|
|
120
132
|
Every substantive finding should point to a verified source path, URL, or document reference.
|
|
121
133
|
|
|
122
|
-
## 10.
|
|
134
|
+
## 10. Community sources, credibility grading, and signal
|
|
135
|
+
|
|
136
|
+
Community sources (Reddit, Stack Overflow, Hacker News, Discord, GitHub Discussions) are admissible as **primary evidence of sentiment, demand, friction, and adoption experience** (§2) — never as evidence for factual, version, security, pricing, or compatibility claims, where the primary must be located.
|
|
137
|
+
|
|
138
|
+
### Admissibility checklist
|
|
139
|
+
|
|
140
|
+
A community source supports a load-bearing sentiment/demand claim only when:
|
|
141
|
+
|
|
142
|
+
- the venue is identified (which subreddit/tag/thread) and the post date is recorded;
|
|
143
|
+
- the signal is corroborated — multiple independent threads/posts, or one high-engagement thread (substantial upvotes and a comment consensus), not a single low-engagement post;
|
|
144
|
+
- it is recent enough for the topic's pace (§1);
|
|
145
|
+
- the claim is about expressed experience or opinion, not a fact the poster is merely repeating.
|
|
146
|
+
|
|
147
|
+
A single anonymous low-engagement post is noise. Cross-thread agreement, repeated independently-raised pain points, and high-engagement consensus are signal. State which you have.
|
|
148
|
+
|
|
149
|
+
### Admiralty grade
|
|
150
|
+
|
|
151
|
+
Grade every source on two independent axes, adapted from the NATO Admiralty Code, and record them together (e.g. `A1`, `B2`, `D4`). The axes are independent: a usually-reliable source can carry improbable information (`B5`), and an unreliable source can report something independently confirmed (`E1`).
|
|
152
|
+
|
|
153
|
+
Source reliability:
|
|
154
|
+
|
|
155
|
+
| Grade | Meaning |
|
|
156
|
+
|---|---|
|
|
157
|
+
| A | Completely reliable — authoritative primary, history of reliability (official docs, standards, peer-reviewed) |
|
|
158
|
+
| B | Usually reliable — minor doubt (reputable vendor docs, established maintainers) |
|
|
159
|
+
| C | Fairly reliable — some doubt, valid in the past (reputable secondary reporting) |
|
|
160
|
+
| D | Not usually reliable — significant doubt (unvetted blogs, single-author claims) |
|
|
161
|
+
| E | Unreliable — history of invalid information |
|
|
162
|
+
| F | Cannot be judged — no basis to evaluate (anonymous, no track record) |
|
|
163
|
+
|
|
164
|
+
Information credibility:
|
|
165
|
+
|
|
166
|
+
| Grade | Meaning |
|
|
167
|
+
|---|---|
|
|
168
|
+
| 1 | Confirmed by other independent sources; consistent with known information |
|
|
169
|
+
| 2 | Probably true — not confirmed, but logical and consistent |
|
|
170
|
+
| 3 | Possibly true — not confirmed, reasonably logical, partial agreement |
|
|
171
|
+
| 4 | Doubtful — not confirmed, possible but not logical, uncorroborated |
|
|
172
|
+
| 5 | Improbable — contradicted by other information |
|
|
173
|
+
| 6 | Cannot be judged — insufficient basis |
|
|
174
|
+
|
|
175
|
+
Confidence mapping: a load-bearing claim may be stated `high` only on `A1`/`A2`/`B1`; `medium` on `B2`/`C2`/`C3`; otherwise `low`. Community sentiment sources are typically `D`–`F` on reliability but can reach `1`–`2` on credibility when cross-corroborated — record both honestly rather than inflating the claim.
|
|
176
|
+
|
|
177
|
+
References: [NATO Admiralty Code](https://en.wikipedia.org/wiki/Admiralty_code), [primary-source definition](https://en.wikipedia.org/wiki/Primary_source).
|
|
178
|
+
|
|
179
|
+
## 11. Anti-patterns
|
|
123
180
|
|
|
124
181
|
Do not:
|
|
125
182
|
|
|
@@ -31,8 +31,20 @@ Additional failure modes on top of the data engineer core.
|
|
|
31
31
|
**Why it fails**: data breaks at the consumer boundary.
|
|
32
32
|
**Counter-move**: publish contracts and run compatibility checks before deploy.
|
|
33
33
|
|
|
34
|
+
## Methodology
|
|
35
|
+
|
|
36
|
+
The monitors above tell you a job broke; lineage and SLAs tell you what it broke and whether that matters.
|
|
37
|
+
|
|
38
|
+
**Lineage.** Every dataset should trace column-to-column from source to consumer, so that when a value is wrong you can answer "what fed this" and "what depends on this" without log archaeology. Capture lineage as metadata (which job, which inputs, which transform), not tribal knowledge; it is what makes an incident's blast radius computable.
|
|
39
|
+
|
|
40
|
+
**Data SLAs / SLOs.** A pipeline without a stated freshness and completeness target has no definition of "broken." Set an SLA per consumed dataset (e.g. "fresh within 1h, 99.5% of rows present") and alert against the SLO, not against raw job status — a job that "succeeded" but delivered half the rows is a breach. Tie the SLA to the consumer's actual decision cadence, not to convenience.
|
|
41
|
+
|
|
42
|
+
**Observability maturity.** Progress from "is the job green" → "is the data fresh and complete" → "is the distribution sane" (volume/null-rate/value drift). The last catches the silent corruption the first two miss.
|
|
43
|
+
|
|
34
44
|
## Self-check before shipping
|
|
35
45
|
- [ ] Reruns, retries, and backfills are idempotent
|
|
36
|
-
- [ ]
|
|
46
|
+
- [ ] Column-level lineage from source to consumer is captured as metadata
|
|
47
|
+
- [ ] A freshness/completeness SLA exists per consumed dataset; alerts fire on SLO breach, not just job failure
|
|
48
|
+
- [ ] Distribution monitors (volume, null-rate, value drift) exist, not just success/failure
|
|
37
49
|
- [ ] Data contracts and compatibility tests are present
|
|
38
50
|
- [ ] Ownership and runbook are clear
|
package/skills/roles/debugger.md
CHANGED
|
@@ -57,6 +57,15 @@ Load this before drafting. These are the failure modes that separate strong role
|
|
|
57
57
|
**Why it fails**: the same bug returns in six months, silently.
|
|
58
58
|
**Counter-move**: add a test that fails against the broken code and passes against the fix. Keep it.
|
|
59
59
|
|
|
60
|
+
## Methodology
|
|
61
|
+
|
|
62
|
+
Root cause is found by building a causal chain, not by guessing:
|
|
63
|
+
|
|
64
|
+
- **Earliest anomaly first**, then work *forward* along cause→effect. The first error in the log is usually an effect; trace upstream to the first place reality diverged from expectation.
|
|
65
|
+
- **Five whys, but each "why" is a tested link, not a story.** "Null pointer → the cache was empty → the warmer never ran → its trigger was disabled → the deploy disabled it." Every arrow must be confirmed by evidence (a log, a value, a repro), or the chain is fiction.
|
|
66
|
+
- **Distinguish the trigger from the root cause** (as in a postmortem): the input that set it off vs. the system condition that let that input cause harm. Fix the root cause; note the trigger.
|
|
67
|
+
- **Stop at the deepest link you can change.** Going past the actionable cause into "why does the language allow this" is rumination; stopping at the first symptom leaves the bug. The root cause is the earliest link whose change prevents recurrence.
|
|
68
|
+
|
|
60
69
|
## Self-check before shipping
|
|
61
70
|
|
|
62
71
|
- [ ] Cause stated in one sentence before the fix
|
|
@@ -37,8 +37,18 @@ Additional failure modes on top of the designer core.
|
|
|
37
37
|
**Why it fails**: triggers vestibular disorders; drives users away.
|
|
38
38
|
**Counter-move**: honor `prefers-reduced-motion`. Provide pause controls for any auto-playing content.
|
|
39
39
|
|
|
40
|
+
## Methodology
|
|
41
|
+
|
|
42
|
+
Automated checks (axe, Lighthouse) catch perhaps a third of WCAG issues; the rest are found by use, not by scan:
|
|
43
|
+
|
|
44
|
+
- **Test with a real screen reader**, not just the accessibility tree — drive the flow with VoiceOver or NVDA and confirm the announced order, labels, and state changes make sense aurally. The DOM can be valid while the spoken experience is incoherent.
|
|
45
|
+
- **Keyboard-only, full task**: complete the whole task with no pointer. Watch focus order, visible focus, and focus traps (modals must trap and restore focus). A reachable control that focus never lands on is unreachable.
|
|
46
|
+
- **Cover the four POUR principles** (Perceivable, Operable, Understandable, Robust) against WCAG 2.x AA — not just contrast and alt text. Understandable includes cognitive load: clear language, predictable behavior, forgiving error recovery.
|
|
47
|
+
- **Test at 200% zoom and with reduced-motion set**; reflow and motion are where "looks accessible" breaks.
|
|
48
|
+
|
|
40
49
|
## Self-check before shipping
|
|
41
|
-
- [ ] Keyboard-only path
|
|
42
|
-
- [ ] Screen-reader output verified
|
|
50
|
+
- [ ] Keyboard-only path completes the full task; focus order, visible focus, and traps checked
|
|
51
|
+
- [ ] Screen-reader output verified by listening (VoiceOver/NVDA), not just the a11y tree
|
|
52
|
+
- [ ] WCAG 2.x AA across POUR, including cognitive load — not only contrast/alt text
|
|
53
|
+
- [ ] Tested at 200% zoom and with reduced-motion
|
|
43
54
|
- [ ] Semantic HTML first; ARIA only where needed
|
|
44
|
-
- [ ] Reduced-motion path exists and is tested
|
package/skills/roles/designer.md
CHANGED
|
@@ -58,8 +58,18 @@ Load this before drafting. These are the failure modes that separate strong role
|
|
|
58
58
|
**Why it fails**: the product feels inconsistent even when individual screens are fine. Engineering cannot implement cleanly.
|
|
59
59
|
**Counter-move**: name the tokens. space, color, type, radius, motion. before designing. Use them.
|
|
60
60
|
|
|
61
|
+
## Methodology
|
|
62
|
+
|
|
63
|
+
Design at the system level, not the screen level:
|
|
64
|
+
|
|
65
|
+
- **Compose, don't draw.** Build from tokens → primitives → components → patterns (atomic design): a screen is an assembly of reused components, not a bespoke canvas. A new one-off where a component exists is debt; a new component should earn its place by appearing in ≥2 contexts.
|
|
66
|
+
- **States are part of the component, not an afterthought.** Each component specifies its empty, loading, error, disabled, and populated states up front — the happy state alone is an incomplete design.
|
|
67
|
+
- **Tokens carry meaning.** Name tokens by role (`color.text.danger`), not by value (`red-600`), so a theme or rebrand changes one definition, not every usage.
|
|
68
|
+
- **Maturity check**: ad-hoc styles → shared components → a governed design system with usage docs and contribution rules. Name the current rung; "we have a component library" with widespread one-offs is rung two.
|
|
69
|
+
|
|
61
70
|
## Self-check before shipping
|
|
62
71
|
|
|
72
|
+
- [ ] New UI composed from existing tokens/components; new components justified by reuse
|
|
63
73
|
- [ ] Visual direction is explicit, not default
|
|
64
74
|
- [ ] Empty / loading / error states designed as first-class
|
|
65
75
|
- [ ] Primary actions visible at rest, not hover-gated
|
|
@@ -42,8 +42,16 @@ Additional failure modes on top of the engineer core.
|
|
|
42
42
|
**Why it fails**: platform surface area compounds blast radius. one leaked token touches every repo.
|
|
43
43
|
**Counter-move**: treat platform secrets as production secrets. Rotate, scope-minimize, and audit.
|
|
44
44
|
|
|
45
|
+
## Methodology
|
|
46
|
+
|
|
47
|
+
**IaC maturity.** Infrastructure should climb a ladder: manual → scripted → declarative (Terraform/Pulumi) → declarative + policy-as-code + drift detection. The rung that matters is the last: state is reconciled (no manual console changes survive) and drift between declared and actual is detected and alerted, not discovered during an incident. Name the current rung honestly; "we have some Terraform" alongside hand-edited resources is rung two, not four.
|
|
48
|
+
|
|
49
|
+
**Supply chain.** Every build emits an SBOM (software bill of materials) so a new CVE can be answered with "are we affected, where" in minutes, not a manual audit. Pin and verify dependencies (lockfiles, checksums, ideally signed provenance), and run the dependency/CVE audit in CI as a gate, not a report. The platform's blast radius is every repo it serves — a compromised build step is the highest-leverage attack.
|
|
50
|
+
|
|
45
51
|
## Self-check before shipping
|
|
46
52
|
- [ ] First consumer migrated and measured
|
|
53
|
+
- [ ] Infra is declarative with drift detection; no surviving manual changes
|
|
54
|
+
- [ ] Build emits an SBOM; dependencies pinned/verified; CVE audit gates CI
|
|
47
55
|
- [ ] Deprecation window respected for any breaking change
|
|
48
56
|
- [ ] Failure diagnostics and artifacts preserved
|
|
49
57
|
- [ ] Build-time and cost deltas measured
|
package/skills/roles/operator.md
CHANGED
|
@@ -69,10 +69,19 @@ Load this before producing operator output. SRE, ops, release, and durable-knowl
|
|
|
69
69
|
|
|
70
70
|
**Why it fails**: no systematic comparison between planned work (documents) and actual work (tickets). Gaps accumulate silently until delivery dates slip.
|
|
71
71
|
|
|
72
|
-
**Counter-move**: run periodic gap analysis: query strategy/PRDs/RFCs from knowledge base, compare with Jira tickets via search, identify missing tickets. Create
|
|
72
|
+
**Counter-move**: run periodic gap analysis: query strategy/PRDs/RFCs from knowledge base, compare with Jira tickets via search, identify missing tickets. Create gap-filling tickets automatically (or queue for approval). Treat "execution gap" as a first-class risk signal.
|
|
73
|
+
|
|
74
|
+
## Methodology
|
|
75
|
+
|
|
76
|
+
Sequencing work is a calculation, not a vibe:
|
|
77
|
+
|
|
78
|
+
- **Critical path.** Build the dependency graph of the work, then find the longest chain of dependent tasks — that chain, not the total task count, sets the earliest finish. Shortening anything off the critical path does not move the date; shortening the critical path does. Re-find it after every scope change, because it moves.
|
|
79
|
+
- **Slack.** Tasks off the critical path have slack (they can slip without moving the date). Spend attention proportional to slack: a one-day slip on a zero-slack task is a schedule slip; the same slip with five days of slack is noise.
|
|
80
|
+
- **Resource leveling.** Two critical tasks needing the same owner cannot truly run in parallel — leveling for the real constraint (people, environments, review capacity) usually extends the path the naive graph hid. Sequence to the actual bottleneck.
|
|
73
81
|
|
|
74
82
|
## Self-check before shipping
|
|
75
83
|
|
|
84
|
+
- [ ] Critical path identified; the date is driven by it, not by task count
|
|
76
85
|
- [ ] Each runbook step names its purpose and expected output
|
|
77
86
|
- [ ] Rollback is a tested, first-class plan with trigger criteria
|
|
78
87
|
- [ ] Every alert is actionable; non-actionable signals moved to dashboards
|