@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.
Files changed (78) hide show
  1. package/README.md +6 -4
  2. package/bin/construct +26 -3
  3. package/db/schema/003_observation_reconciliation.sql +14 -0
  4. package/lib/bootstrap/resources.mjs +0 -1
  5. package/lib/cli-commands.mjs +33 -5
  6. package/lib/comment-lint.mjs +44 -0
  7. package/lib/contracts/validate.mjs +106 -0
  8. package/lib/decisions/enforced-baseline.json +23 -0
  9. package/lib/decisions/golden.mjs +87 -0
  10. package/lib/decisions/precedence.mjs +46 -0
  11. package/lib/decisions/registry.mjs +469 -0
  12. package/lib/deployment/parity-contract.mjs +148 -0
  13. package/lib/embed/cli.mjs +11 -0
  14. package/lib/embed/conflict-detection.mjs +4 -4
  15. package/lib/embed/customer-profiles.mjs +1 -1
  16. package/lib/embed/reconcile.mjs +60 -0
  17. package/lib/gates-audit.mjs +2 -2
  18. package/lib/hooks/config-protection.mjs +22 -3
  19. package/lib/hooks/guard-bash.mjs +1 -1
  20. package/lib/init-docs.mjs +1 -0
  21. package/lib/mode-commands.mjs +6 -8
  22. package/lib/observation-store.mjs +16 -2
  23. package/lib/opencode-telemetry.mjs +1 -1
  24. package/lib/roles/cli.mjs +10 -2
  25. package/lib/roles/gateway.mjs +50 -1
  26. package/lib/scheduler/index.mjs +31 -0
  27. package/lib/server/index.mjs +13 -3
  28. package/lib/server/static/index.html +1 -1
  29. package/lib/setup.mjs +6 -0
  30. package/lib/storage/hybrid-query.mjs +49 -38
  31. package/lib/storage/rrf.mjs +42 -0
  32. package/lib/storage/vector-client.mjs +18 -3
  33. package/lib/telemetry/backends/local.mjs +1 -1
  34. package/lib/telemetry/skill-calls.mjs +1 -1
  35. package/lib/templates/visual-requirements.mjs +84 -0
  36. package/package.json +9 -1
  37. package/rules/common/comments.md +3 -0
  38. package/rules/common/no-fabrication.md +3 -0
  39. package/rules/common/precedence.md +19 -0
  40. package/rules/common/research-sources.md +32 -0
  41. package/rules/common/research.md +59 -2
  42. package/skills/roles/data-engineer.pipeline.md +13 -1
  43. package/skills/roles/debugger.md +9 -0
  44. package/skills/roles/designer.accessibility.md +13 -3
  45. package/skills/roles/designer.md +10 -0
  46. package/skills/roles/engineer.platform.md +8 -0
  47. package/skills/roles/operator.md +10 -1
  48. package/skills/roles/operator.release.md +8 -0
  49. package/skills/roles/operator.sre.md +10 -1
  50. package/skills/roles/orchestrator.md +9 -2
  51. package/skills/roles/product-manager.business-strategy.md +10 -1
  52. package/skills/roles/researcher.explorer.md +12 -1
  53. package/skills/roles/researcher.ux.md +13 -1
  54. package/skills/roles/reviewer.devil-advocate.md +14 -2
  55. package/skills/roles/reviewer.evaluator.md +17 -4
  56. package/skills/roles/reviewer.trace.md +12 -1
  57. package/skills/roles/security.legal-compliance.md +8 -0
  58. package/skills/roles/security.md +11 -0
  59. package/specialists/contracts.json +18 -0
  60. package/specialists/prompts/cx-researcher.md +4 -2
  61. package/templates/docs/backlog-proposal.md +1 -1
  62. package/templates/docs/customer-profile.md +1 -1
  63. package/templates/docs/evidence-brief.md +5 -1
  64. package/templates/docs/incident-report.md +37 -21
  65. package/templates/docs/prfaq.md +2 -2
  66. package/templates/docs/product-intelligence-report.md +1 -1
  67. package/templates/docs/research-brief.md +8 -6
  68. package/templates/docs/research-finding.md +32 -7
  69. package/templates/docs/rfc.md +13 -1
  70. package/templates/docs/runbook.md +20 -1
  71. package/templates/docs/signal-brief.md +4 -1
  72. package/templates/docs/skill-artifact.md +27 -7
  73. package/templates/docs/strategy.md +23 -2
  74. package/lib/bootstrap/lazy-install.mjs +0 -161
  75. package/lib/embed/jobs/vector-sync.mjs +0 -198
  76. package/lib/knowledge/postgres-search.mjs +0 -132
  77. package/lib/services/pattern-promotion-service.mjs +0 -167
  78. 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 modelInfo = await getEmbeddingModelInfo({ env });
115
- const embeddingModel = modelInfo.model;
130
+ const embeddingModel = modelOverride || (await getEmbeddingModelInfo({ env })).model;
116
131
 
117
132
  try {
118
- const embeddingRows = await client`
119
- select d.id, d.kind, d.title, d.summary, d.body, d.source_path, d.tags, e.embedding
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 queryEmbedding = (await embedText(query, { env })).embedding;
126
- const embeddedHits = scoreByEmbedding(
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
- const merged = [...base.results];
142
- for (const hit of embeddedHits) {
143
- if (merged.some((entry) => entry.id === hit.id)) continue;
144
- merged.push({
145
- id: hit.id,
146
- kind: hit.kind,
147
- title: hit.title,
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: merged.slice(0, limit),
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(TRACES_DIR)
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'|'pattern-promotion'|'validation'|'other'} event.source
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.18",
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
  }
@@ -1,5 +1,8 @@
1
1
  ---
2
2
  description: Construct comment convention for JS/TS/MJS source files.
3
+ enforced_by: lib/comment-lint.mjs
4
+ adr_reference: ADR-0015
5
+ precedence_tier: style
3
6
  ---
4
7
  # Comment Convention
5
8
 
@@ -1,5 +1,8 @@
1
1
  ---
2
2
  description: canonical anti-fabrication policy for Construct.
3
+ enforced_by: lib/comment-lint.mjs, lib/contracts/validate.mjs
4
+ adr_reference: ADR-0015
5
+ precedence_tier: correctness
3
6
  ---
4
7
  # No-Fabrication Policy
5
8
 
@@ -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)
@@ -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. Anti-patterns
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
- - [ ] Freshness, volume, schema, latency, and error monitors exist
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
@@ -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 tested for every interactive element
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
@@ -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
@@ -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补缺 tickets automatically (or queue for approval). Treat "execution gap" as a first-class risk signal.
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