@thanbir-tamim/enterprise-project-intelligence-agent 0.1.1 → 0.2.0

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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +302 -49
  3. package/assets/figures/feasibility-scoring-engine.png +0 -0
  4. package/assets/figures/hero-architecture.png +0 -0
  5. package/assets/figures/skills-interaction-map.png +0 -0
  6. package/docs/SUPPORTED_FORMATS.md +4 -4
  7. package/examples/synthetic-inventory-system/ai-opportunity-matrix/AI_OPPORTUNITY_MATRIX.md +4 -4
  8. package/examples/synthetic-inventory-system/generated-docs/INVENTORY_SUMMARY.md +17 -0
  9. package/examples/synthetic-inventory-system/generated-docs/evidence-index.json +78 -0
  10. package/examples/synthetic-inventory-system/generated-docs/inventory.json +26 -0
  11. package/examples/synthetic-inventory-system/presentation/DEMO_DECK.md +69 -55
  12. package/examples/synthetic-inventory-system/presentation/demo-deck.json +194 -16
  13. package/package.json +2 -1
  14. package/scripts/cli.mjs +20 -14
  15. package/scripts/lib/skills.mjs +1 -1
  16. package/scripts/release-audit.mjs +73 -70
  17. package/skills/corporate-presentation-generator/SKILL.md +49 -23
  18. package/skills/corporate-presentation-generator/scripts/outline-to-deck-json.mjs +77 -0
  19. package/skills/documentation-quality-auditor/SKILL.md +28 -21
  20. package/skills/documentation-quality-auditor/references/audit-checks.md +15 -8
  21. package/skills/documentation-quality-auditor/scripts/audit-docs.mjs +192 -32
  22. package/skills/engineering-knowledge-transfer/SKILL.md +1 -5
  23. package/skills/enterprise-ai-integration-planner/SKILL.md +13 -15
  24. package/skills/enterprise-ai-integration-planner/references/feasibility-scoring.md +26 -5
  25. package/skills/enterprise-ai-integration-planner/scripts/score-opportunity.mjs +32 -15
  26. package/skills/presentation-compliance-validator/SKILL.md +15 -15
  27. package/skills/presentation-compliance-validator/references/rule-schema.md +67 -10
  28. package/skills/presentation-compliance-validator/scripts/validate-deck.mjs +48 -11
  29. package/skills/project-documentation-discovery/SKILL.md +25 -8
  30. package/skills/project-documentation-discovery/references/format-matrix.md +4 -4
  31. package/skills/project-documentation-discovery/scripts/inventory.mjs +174 -100
  32. package/skills/project-documentation-discovery/scripts/secrets.mjs +99 -0
  33. package/skills/project-knowledge-assistant/SKILL.md +1 -7
  34. package/skills/software-change-impact-analyzer/SKILL.md +15 -11
  35. package/skills/software-reverse-engineering/SKILL.md +14 -10
  36. package/skills/software-reverse-engineering/references/system-map-schema.md +27 -0
  37. package/skills/technical-document-generator/SKILL.md +6 -8
  38. package/skills/technical-document-generator/scripts/render-markdown-to-html.mjs +76 -66
@@ -1,52 +1,212 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Reference documentation audit: broken internal references + unsupported strong claims,
4
- * operating on the inventory/evidence-index produced by project-documentation-discovery.
5
- * Pure function over structured input; no network calls.
3
+ * Deterministic documentation audit for documentation-quality-auditor.
4
+ *
5
+ * Checks (see ../references/audit-checks.md):
6
+ * broken-internal-reference link target file missing, or #anchor not a heading in the target
7
+ * unresolvable-citation [source: path#anchor] whose file/anchor does not exist
8
+ * internal-looking-external-link private/internal host or IP (value redacted in output)
9
+ * unsupported-claim absolute/strong claim with no citation on the line
10
+ * contradiction-candidate same subject stated with different numeric values in different places
11
+ * missing-required-section expected heading absent (when requiredSections is supplied)
12
+ * possibly-stale file older than staleAfterDays (when supplied) — mtime based
13
+ * It does not judge whether a citation semantically SUPPORTS a claim; that remains an LLM/human step.
14
+ *
15
+ * CLI: node audit-docs.mjs <corpusDir> [--required <file.json>] [--stale-after-days N] [--out report.json]
16
+ * requiredSections JSON: { "<relative path or glob>": ["Heading A", "Heading B"] }
6
17
  */
7
18
  import { pathToFileURL } from "node:url";
8
- import { existsSync } from "node:fs";
9
- import { join } from "node:path";
19
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
20
+ import { join, dirname, normalize, relative } from "node:path";
10
21
 
11
- const STRONG_CLAIM_RE = /\b(always|never|guaranteed|100%|zero downtime|fully compliant)\b/i;
22
+ // Absolute assurance phrases are always checked; bare absolutes (always/never/every/all) only when the
23
+ // sentence also states a metric or a reliability/performance property. Imperatives ("Never fabricate X")
24
+ // are rules, not factual claims, and negated assurances ("not a guarantee") are not claims either.
25
+ const ASSURANCE_RE = /\b(guarantee[sd]?|100\s?%|zero[- ]downtime|fully compliant|never (?:fails?|goes down|loses?)|always (?:available|up|on))\b/i;
26
+ const ABSOLUTE_RE = /\b(always|never|every|all|instant(?:ly)?)\b/i;
27
+ const PROPERTY_RE = /\b(respon\w*|available|availability|uptime|latency|fail\w*|lose|loss|lost|accura\w*|secure|succe\w*|fast\w*|reliab\w*|scal\w*|real[- ]time)\b/i;
28
+ const METRIC_RE = /\d+(?:\.\d+)?\s*(%|ms|milliseconds?|seconds?|s\b|minutes?|hours?|days?|rps|qps)/i;
29
+ const IMPERATIVE_RE = /^\W*(?:\d+[.)]\s*)?(?:\[ \]\s*)?(?:\*\*)?(never|always|do not|don't|must|should|avoid|ensure|keep|use|treat|report|record|flag)\b/i;
30
+ const MODAL_RE = /\b(must|should|shall|may|do not|don't|is not allowed|are not allowed)\s+(?:\w+\s+){0,2}(never|always)\b|\b(never|always)\s+(?:be\s+)?(?:\w+ed|bundle|embed|commit|invent|claim|present|state|draw|answer|resolve|upgrade|fabricate|copy|modify|substitute|surface|rely|allow|assume|guess|let|recommend|publish|silently)\b/i;
31
+ const NEGATED_RE = /\b(not|no|never|cannot|can't|isn't|aren't|doesn't|without)\b[^.]{0,25}\b(guarantee\w*|100\s?%|proof)/i;
32
+ export function isUnsupportedStrongClaim(sentence) {
33
+ if (CITATION_RE.test(sentence) || IMPERATIVE_RE.test(sentence) || NEGATED_RE.test(sentence) || MODAL_RE.test(sentence)) return false;
34
+ if (ASSURANCE_RE.test(sentence)) return true;
35
+ return ABSOLUTE_RE.test(sentence) && (METRIC_RE.test(sentence) || PROPERTY_RE.test(sentence));
36
+ }
37
+ const CITATION_RE = /\[(?:source|src)\s*:[^\]]+\]/i;
38
+ const INTERNAL_HOST_RE = /\/\/(?:[^/\s)]*\.(?:internal|corp|local|lan|intranet)\b|localhost\b|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+)/i;
39
+ const UNIT_RE = /(\d+(?:\.\d+)?)\s*(%|ms|milliseconds?|seconds?|minutes?|hours?|days?|weeks?|months?|years?|gb|mb|tb|requests?|users?|tickets?|retries|attempts|times)\b/gi;
40
+ const STOP = new Set("the a an and or of to for in on at by with is are be been was were it its this that these those than then after before into from as per each every all any no not only also can may must will should would could within up down over under about we our you your they their there here which who what when where how via them its they him her his she he one two".split(" "));
41
+
42
+ export const slugify = (t) => t.toLowerCase().trim().replace(/[`*~]/g, "").replace(/[^a-z0-9 _-]/g, "").replace(/ /g, "-"); // GitHub-compatible
43
+ const stem = (w) => w.replace(/(ing|ed|es|s|e|d)$/, "");
44
+ const contentWords = (s) => new Set(s.toLowerCase().replace(/\[[^\]]*\]|\([^)]*\)|`[^`]*`/g, " ").split(/[^a-z]+/).filter((w) => w.length > 2 && !STOP.has(w)).map(stem));
45
+ const unitKey = (u) => u.toLowerCase().replace(/^(milliseconds?)$/, "ms").replace(/s$/, "");
46
+
47
+ function headingsOf(text) {
48
+ const out = []; let fence = false;
49
+ for (const l of text.split("\n")) {
50
+ if (/^\s*(```|~~~)/.test(l)) { fence = !fence; continue; }
51
+ if (fence) continue;
52
+ const m = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l); if (m) out.push(m[2]);
53
+ }
54
+ for (const m of text.matchAll(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gis)) out.push(m[1].replace(/<[^>]+>/g, ""));
55
+ return out;
56
+ }
12
57
 
13
- export function auditDocs({ corpusPath, inventory, evidenceIndex, fileTexts }) {
58
+ /** Splits text into sentences with the line each sentence starts on; wrapped lines are joined. */
59
+ function sentencesWithLines(text) {
60
+ const units = []; let fence = false; let cur = null;
61
+ text.split("\n").forEach((line, i) => {
62
+ if (/^\s*(```|~~~)/.test(line)) { fence = !fence; cur = null; return; }
63
+ if (fence) return;
64
+ if (/^\s*#{1,6}\s/.test(line) || /^\s*<!--/.test(line) || !line.trim()) { cur = null; return; }
65
+ if (/^\s*\|/.test(line)) { cur = null; units.push({ text: line.replace(/\|/g, " ").trim(), marks: [{ pos: 0, line: i + 1 }], table: true }); return; }
66
+ const startsItem = /^\s*(?:[-*+]|\d+[.)]|>)\s+/.test(line);
67
+ const piece = line.replace(/^\s*(?:[-*+]|>)\s+/, "").trim();
68
+ if (cur && !startsItem) { cur.marks.push({ pos: cur.text.length + 1, line: i + 1 }); cur.text += " " + piece; }
69
+ else { cur = { text: piece, marks: [{ pos: 0, line: i + 1 }] }; units.push(cur); }
70
+ });
71
+ const out = [];
72
+ for (const u of units) {
73
+ const re = /(?<=[.!?;])\s+(?=[A-Z`*(])|\s+—\s+/g; let start = 0; let m;
74
+ const push = (end) => { const t = u.text.slice(start, end).trim(); if (t) { const line = [...u.marks].reverse().find((k) => k.pos <= start)?.line ?? u.marks[0].line; out.push({ line, text: t, table: u.table }); } };
75
+ while ((m = re.exec(u.text))) { push(m.index); start = m.index + m[0].length; }
76
+ push(u.text.length);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ const globRe = (g) => new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*") + "$");
82
+
83
+ export function auditDocs({ corpusPath, fileTexts, requiredSections = {}, staleAfterDays = null, now = Date.now() }) {
14
84
  const findings = [];
85
+ const files = Object.keys(fileTexts || {});
86
+ const headingCache = new Map();
87
+ const headingSlugs = (rel) => {
88
+ if (!headingCache.has(rel)) {
89
+ const text = fileTexts[rel] ?? (existsSync(join(corpusPath, rel)) && statSync(join(corpusPath, rel)).isFile() ? readFileSync(join(corpusPath, rel), "utf8") : "");
90
+ headingCache.set(rel, new Set(headingsOf(text).map(slugify)));
91
+ }
92
+ return headingCache.get(rel);
93
+ };
94
+ const resolveTarget = (from, target) => {
95
+ const [pathPart, anchor] = target.split("#");
96
+ const candidates = pathPart ? [normalize(join(dirname(from), pathPart)), normalize(pathPart)] : [from];
97
+ const hit = candidates.find((c) => !c.startsWith("..") && existsSync(join(corpusPath, c)));
98
+ return { file: hit ?? null, anchor: anchor ? decodeURIComponent(anchor) : null, pathPart };
99
+ };
15
100
 
16
- // Broken internal references: look for markdown-style links in fileTexts and verify they resolve.
17
- for (const [relPath, text] of Object.entries(fileTexts || {})) {
18
- const linkRe = /\]\(([^)]+)\)/g;
19
- let m;
20
- while ((m = linkRe.exec(text))) {
21
- const target = m[1];
22
- if (/^https?:\/\//.test(target)) {
23
- if (/\b(10\.\d+\.\d+\.\d+|192\.168\.|localhost|\.internal\b|\.corp\b)\b/.test(target)) {
24
- findings.push({ category: "internal-looking-external-link", severity: "medium", source_path: relPath, detail: "Link matches internal-hostname heuristic (value redacted).", redacted: true });
101
+ for (const rel of files) {
102
+ const text = fileTexts[rel];
103
+ const lines = text.split("\n");
104
+ let fence = false;
105
+ lines.forEach((line, idx) => {
106
+ if (/^\s*(```|~~~)/.test(line)) { fence = !fence; return; }
107
+ if (fence) return;
108
+ const ln = idx + 1;
109
+ // links
110
+ for (const m of line.matchAll(/\]\(([^)\s]+)\)/g)) {
111
+ const target = m[1];
112
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
113
+ if (INTERNAL_HOST_RE.test(target)) findings.push({ category: "internal-looking-external-link", severity: "medium", source_path: rel, line: ln, detail: "Link points to an internal-looking host (value redacted). Confirm it may appear in shared documentation." });
114
+ continue;
25
115
  }
26
- continue;
116
+ const r = resolveTarget(rel, target);
117
+ if (!r.file) findings.push({ category: "broken-internal-reference", severity: "high", source_path: rel, line: ln, detail: `Linked file "${r.pathPart}" does not exist in the corpus.` });
118
+ else if (r.anchor && /\.(md|markdown|html?)$/i.test(r.file) && !headingSlugs(r.file).has(slugify(r.anchor))) findings.push({ category: "broken-internal-reference", severity: "medium", source_path: rel, line: ln, detail: `Anchor "#${r.anchor}" not found among headings of "${r.file}".` });
27
119
  }
28
- const resolved = join(corpusPath, relPath, "..", target);
29
- if (!existsSync(resolved) && !existsSync(join(corpusPath, target))) {
30
- findings.push({ category: "broken-internal-reference", severity: "high", source_path: relPath, detail: `Reference "${target}" does not resolve within the corpus.` });
120
+ if (/^\s*#{1,6}\s/.test(line)) return; // headings are not claims
121
+ // citations
122
+ for (const m of line.matchAll(/\[(?:source|src)\s*:\s*([^\]]+)\]/gi)) {
123
+ for (const ref of m[1].split(/[;,]\s*/)) {
124
+ const target = ref.trim().split(/\s/)[0];
125
+ if (!target || !/[./#]/.test(target)) continue;
126
+ const r = resolveTarget(rel, target);
127
+ if (!r.file) findings.push({ category: "unresolvable-citation", severity: "high", source_path: rel, line: ln, detail: `Citation target "${r.pathPart}" does not exist.` });
128
+ else if (r.anchor && /\.(md|markdown|html?)$/i.test(r.file) && !/^L\d/.test(r.anchor) && !headingSlugs(r.file).has(slugify(r.anchor))) findings.push({ category: "unresolvable-citation", severity: "medium", source_path: rel, line: ln, detail: `Citation anchor "#${r.anchor}" not found in "${r.file}".` });
129
+ else findings.push({ category: "citation-needs-support-check", severity: "info", source_path: rel, line: ln, detail: `Citation to "${target}" resolves; whether it actually supports the claim needs review.` });
130
+ }
31
131
  }
32
- }
132
+ });
133
+ }
33
134
 
34
- for (const line of text.split("\n")) {
35
- if (STRONG_CLAIM_RE.test(line) && !/\[source:/i.test(line) && !/#/.test(line)) {
36
- findings.push({ category: "unsupported-claim", severity: "medium", source_path: relPath, detail: `Strong claim without citation: "${line.trim().slice(0, 120)}"` });
37
- }
135
+ // unsupported strong claims (sentence level; table rows included)
136
+ for (const rel of files) for (const sn of sentencesWithLines(fileTexts[rel])) if (isUnsupportedStrongClaim(sn.text)) findings.push({ category: "unsupported-claim", severity: "medium", source_path: rel, line: sn.line, detail: `Strong claim without citation: "${sn.text.slice(0, 140)}"` });
137
+
138
+ // contradiction candidates: similar statements with different numeric values in the same unit.
139
+ // Series/enumeration lines (the same sentence skeleton repeated >=3x in a file, e.g. a timing
140
+ // script or a changelog table) are NOT contradictions — they list different items — so exclude them.
141
+ const skeleton = (t) => t.toLowerCase().replace(/\d+(?:[.,:]\d+)?/g, "#").replace(/\s+/g, " ").trim();
142
+ const skelCount = new Map();
143
+ const rawClaims = [];
144
+ for (const rel of files) for (const s of sentencesWithLines(fileTexts[rel])) for (const m of s.text.matchAll(UNIT_RE)) {
145
+ const sk = rel + "\u0000" + skeleton(s.text); skelCount.set(sk, (skelCount.get(sk) || 0) + 1);
146
+ rawClaims.push({ rel, line: s.line, text: s.text, value: Number(m[1]), unit: unitKey(m[2]), words: contentWords(s.text), sk });
147
+ }
148
+ const claims = rawClaims.filter((c) => skelCount.get(c.sk) < 3); // drop repeated-template series
149
+ const seen = new Set(); const perFile = new Map(); const CAP = 40;
150
+ for (let i = 0; i < claims.length; i++) for (let j = i + 1; j < claims.length; j++) {
151
+ const a = claims[i], b = claims[j];
152
+ if (a.unit !== b.unit || a.value === b.value || (a.rel === b.rel && a.line === b.line)) continue;
153
+ if (a.sk === b.sk) continue; // identical skeleton across lines = same template, not a conflict
154
+ const inter = [...a.words].filter((w) => b.words.has(w)).length; const union = new Set([...a.words, ...b.words]).size;
155
+ if (!union || inter / union < 0.5 || inter < 4) continue; // require a strong subject overlap
156
+ const key = [a.rel, a.line, b.rel, b.line].join("|"); if (seen.has(key)) continue; seen.add(key);
157
+ const fkey = a.rel + "|" + b.rel; const n = perFile.get(fkey) || 0; if (n >= CAP) continue; perFile.set(fkey, n + 1);
158
+ findings.push({ category: "contradiction-candidate", severity: "high", source_path: a.rel, line: a.line, other: { source_path: b.rel, line: b.line }, detail: `"${a.text.slice(0, 100)}" (${a.value} ${a.unit}) vs "${b.text.slice(0, 100)}" (${b.value} ${b.unit}) — report both sides; do not pick one.` });
159
+ }
160
+
161
+ // required sections
162
+ for (const [pattern, required] of Object.entries(requiredSections || {})) {
163
+ const re = globRe(pattern);
164
+ for (const rel of files.filter((f) => f === pattern || re.test(f))) {
165
+ const have = new Set(headingsOf(fileTexts[rel]).map(slugify));
166
+ for (const h of required) if (!have.has(slugify(h))) findings.push({ category: "missing-required-section", severity: "medium", source_path: rel, line: null, detail: `Expected section "${h}" not found.` });
38
167
  }
39
168
  }
40
169
 
41
- return { findings, totalFiles: Object.keys(fileTexts || {}).length };
170
+ // staleness (opt-in; mtime-based)
171
+ if (staleAfterDays != null && corpusPath) for (const rel of files) {
172
+ const p = join(corpusPath, rel); if (!existsSync(p)) continue;
173
+ const ageDays = (now - statSync(p).mtimeMs) / 86400000;
174
+ if (ageDays > staleAfterDays && !/last[- ]reviewed\s*:/i.test(fileTexts[rel])) findings.push({ category: "possibly-stale", severity: "low", source_path: rel, line: null, detail: `Not modified for ${Math.floor(ageDays)} days (threshold ${staleAfterDays}) and no "Last reviewed:" marker.` });
175
+ }
176
+
177
+ const order = { high: 0, medium: 1, low: 2, info: 3 };
178
+ findings.sort((a, b) => order[a.severity] - order[b.severity] || a.source_path.localeCompare(b.source_path) || (a.line ?? 0) - (b.line ?? 0));
179
+ const counts = {}; for (const f of findings) counts[f.category] = (counts[f.category] || 0) + 1;
180
+ return { findings, counts, totalFiles: files.length, notChecked: ["whether a resolvable citation semantically supports its claim", "non-numeric contradictions", ...(staleAfterDays == null ? ["staleness (pass --stale-after-days)"] : []), ...(Object.keys(requiredSections || {}).length ? [] : ["required sections (pass --required)"])] };
42
181
  }
43
182
 
44
- async function main() {
45
- console.error("This script is intended to be imported programmatically with pre-loaded file texts.");
46
- console.error("See skills/documentation-quality-auditor/SKILL.md for the full procedure.");
47
- process.exit(0);
183
+ export function loadCorpusTexts(corpusPath) {
184
+ const out = {};
185
+ (function walk(d) {
186
+ for (const e of readdirSync(d)) {
187
+ if ([".git", "node_modules"].includes(e)) continue;
188
+ const p = join(d, e);
189
+ if (statSync(p).isDirectory()) walk(p);
190
+ else if (/\.(md|markdown|html?|txt)$/i.test(e)) out[relative(corpusPath, p).split("\\").join("/")] = readFileSync(p, "utf8");
191
+ }
192
+ })(corpusPath);
193
+ return out;
48
194
  }
49
195
 
50
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
51
- main();
196
+ function main() {
197
+ const args = process.argv.slice(2);
198
+ const corpusPath = args.find((a, i) => !a.startsWith("--") && !["--required", "--stale-after-days", "--out"].includes(args[i - 1]));
199
+ if (!corpusPath) { console.error("Usage: node audit-docs.mjs <corpusDir> [--required <file.json>] [--stale-after-days N] [--out report.json]"); process.exit(1); }
200
+ const opt = (k) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : undefined; };
201
+ try {
202
+ if (!existsSync(corpusPath) || !statSync(corpusPath).isDirectory()) throw new Error(`Corpus directory not found: ${corpusPath}`);
203
+ const requiredSections = opt("--required") ? JSON.parse(readFileSync(opt("--required"), "utf8")) : {};
204
+ const stale = opt("--stale-after-days");
205
+ const report = auditDocs({ corpusPath, fileTexts: loadCorpusTexts(corpusPath), requiredSections, staleAfterDays: stale != null ? Number(stale) : null });
206
+ const json = JSON.stringify(report, null, 2);
207
+ if (opt("--out")) writeFileSync(opt("--out"), json + "\n");
208
+ console.log(json);
209
+ } catch (err) { console.error(`audit-docs: ${err.message}`); process.exit(1); }
52
210
  }
211
+
212
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
@@ -1,10 +1,6 @@
1
1
  ---
2
2
  name: engineering-knowledge-transfer
3
- description: >-
4
- Generate onboarding guides, operational runbooks, handover documents, troubleshooting guides, and
5
- role-specific learning paths from project evidence. Use this skill when a user needs to bring a new
6
- engineer, operator, or reviewer up to speed on a target project, or when preparing a handover
7
- between teams.
3
+ description: "Generate onboarding guides, operational runbooks, handover documents, troubleshooting guides, and role-specific learning paths from project evidence. Use this skill when a user needs to bring a new engineer, operator, or reviewer up to speed on a target project, or when preparing a handover between teams."
8
4
  metadata:
9
5
  version: 1.0.0
10
6
  partOf: enterprise-project-intelligence-agent
@@ -1,13 +1,6 @@
1
1
  ---
2
2
  name: enterprise-ai-integration-planner
3
- description: >-
4
- Discover, evaluate, design, prioritize, and plan AI integration opportunities for any existing
5
- software project, including projects with no current AI capabilities. Use this skill once project
6
- evidence exists (via project-documentation-discovery and/or software-reverse-engineering) and the
7
- user wants to know where AI could help, whether it should, and how to adopt it safely. It compares
8
- every AI proposal against the simplest viable non-AI alternative, scores feasibility
9
- transparently, treats security/privacy/legal blockers as non-overridable, and never invents ROI,
10
- accuracy, latency, or cost numbers.
3
+ description: "Discover, evaluate, design, prioritize, and plan AI integration opportunities for any existing software project, including projects with no current AI capabilities. Use this skill once project evidence exists (via project-documentation-discovery and/or software-reverse-engineering) and the user wants to know where AI could help, whether it should, and how to adopt it safely. It compares every AI proposal against the simplest viable non-AI alternative, scores feasibility transparently, treats security/privacy/legal blockers as non-overridable, and never invents ROI, accuracy, latency, or cost numbers."
11
4
  metadata:
12
5
  version: 1.0.0
13
6
  partOf: enterprise-project-intelligence-agent
@@ -83,13 +76,18 @@ project into this public repository; use the blank template plus the synthetic e
83
76
 
84
77
  ### 4. Score feasibility transparently (§7.4)
85
78
 
86
- Run `scripts/score-opportunity.mjs` (or apply `references/feasibility-scoring.md` by hand) against
87
- each candidate across: business value, data readiness, integration feasibility, technical maturity,
88
- security readiness, evaluation feasibility, maintainability, cost efficiency. The script enforces the
89
- rule in `references/feasibility-scoring.md` §"Non-overridable blockers": if any critical
90
- security/privacy/legal/authorization blocker is flagged, the opportunity's recommendation is forced
91
- to `blocked` or `rework-required` regardless of the weighted score. Never report a numeric score as
92
- if it alone determines the recommendation.
79
+ **Run** `scripts/score-opportunity.mjs` for every candidate — pass a JSON array of opportunities
80
+ (`[{"id":"O1","businessValue":4,...,"blockers":[...]}]`) or a path to a `.json` file — and paste its
81
+ JSON output (score, band, recommendation, explanation) into the assessment. Never hand-calculate
82
+ weighted scores. Scores use the eight dimensions in `references/feasibility-scoring.md`; score `0`
83
+ only when a dimension could not be assessed (the script then returns `insufficient-evidence`).
84
+
85
+ Blockers have a fixed class (table in `references/feasibility-scoring.md`): structural blockers
86
+ (`no-authorization-path`, `regulatory-prohibition`) force `blocked`; mitigable ones force
87
+ `rework-required`. If a different design would avoid a structural blocker (e.g., a locally hosted
88
+ model instead of a third-party API), list it as a **separate opportunity variant** and score it
89
+ without that blocker only when the evidence supports that the blocker no longer applies. Never
90
+ report a numeric score as if it alone determines the recommendation.
93
91
 
94
92
  ### 5. Design architecture alternatives (§7.5)
95
93
 
@@ -38,11 +38,32 @@ Positive criteria must never cancel out a critical blocker. Define blockers sepa
38
38
  action without human approval.
39
39
  - `blocker:regulatory-prohibition` — a named law, regulation, or binding internal policy prohibits it.
40
40
 
41
- **Rule**: if one or more blockers are present, the recommendation is forced to `blocked` (if the
42
- blocker is structural and cannot be mitigated in this design) or `rework-required` (if a concrete
43
- mitigation would remove the blocker), regardless of the weighted score. `scripts/score-opportunity.mjs`
44
- implements this rule and is covered by an automated test; do not reimplement the rule in a way that
45
- lets a numeric score override it.
41
+ **Rule**: if one or more blockers are present, the recommendation is forced regardless of the weighted
42
+ score. Each blocker has a fixed class — do not reclassify a blocker case-by-case:
43
+
44
+ | Blocker | Class | Forced recommendation | Why |
45
+ |---|---|---|---|
46
+ | `blocker:no-authorization-path` | **structural** | `blocked` | No approved basis for this data use exists *in this design*. Changing the design (e.g., a locally hosted model) is a **new opportunity variant** that must be scored separately — without this blocker, but only if the authorization question is actually resolved for it. |
47
+ | `blocker:regulatory-prohibition` | **structural** | `blocked` | A law/regulation/binding policy forbids it. |
48
+ | `blocker:unresolved-pii-exposure` | mitigable | `rework-required` | A concrete mitigation (minimization, redaction, access scoping) can remove it. |
49
+ | `blocker:no-access-control-on-retrieval` | mitigable | `rework-required` | Retrieval can be made to inherit source permissions. |
50
+ | `blocker:irreversible-autonomous-action` | mitigable | `rework-required` | Add a human approval gate. |
51
+
52
+ This table mirrors `STRUCTURAL_BLOCKERS` / `MITIGABLE_BLOCKERS` in `scripts/score-opportunity.mjs`;
53
+ an automated test fails if they diverge. Do not reimplement the rule in a way that lets a numeric
54
+ score override it.
55
+
56
+ ## Not-assessed dimensions
57
+
58
+ A dimension scored `0` means "not assessed / insufficient evidence", never "very bad". If any
59
+ dimension is `0` (and no structural blocker applies), the script returns
60
+ `recommendation: "insufficient-evidence"` plus the list of `notAssessed` dimensions. Report the
61
+ weighted score only as provisional and list the evidence needed.
62
+
63
+ ## Always use the script
64
+
65
+ Compute every score with `scripts/score-opportunity.mjs` (single JSON object, an array of objects,
66
+ or a path to a `.json` file) and quote its JSON output. Do not hand-calculate weighted scores.
46
67
 
47
68
  ## Required accompanying documentation per proposal
48
69
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { pathToFileURL } from "node:url";
3
+ import { readFileSync } from "node:fs";
3
4
  /**
4
5
  * Transparent, reproducible feasibility scoring for one AI opportunity.
5
6
  * Implements the weighting formula and non-overridable-blocker rule documented in
@@ -13,7 +14,7 @@ import { pathToFileURL } from "node:url";
13
14
  * import { scoreOpportunity } from "./score-opportunity.mjs";
14
15
  */
15
16
 
16
- const WEIGHTS = {
17
+ export const WEIGHTS = {
17
18
  businessValue: 0.20,
18
19
  dataReadiness: 0.15,
19
20
  integrationFeasibility: 0.15,
@@ -24,12 +25,13 @@ const WEIGHTS = {
24
25
  costEfficiency: 0.075,
25
26
  };
26
27
 
27
- const STRUCTURAL_BLOCKERS = new Set([
28
+ /** Blocker taxonomy — documented in references/feasibility-scoring.md (keep in sync; enforced by tests). */
29
+ export const STRUCTURAL_BLOCKERS = new Set([
28
30
  "blocker:no-authorization-path",
29
31
  "blocker:regulatory-prohibition",
30
32
  ]);
31
33
 
32
- const MITIGABLE_BLOCKERS = new Set([
34
+ export const MITIGABLE_BLOCKERS = new Set([
33
35
  "blocker:unresolved-pii-exposure",
34
36
  "blocker:no-access-control-on-retrieval",
35
37
  "blocker:irreversible-autonomous-action",
@@ -59,7 +61,6 @@ export function scoreOpportunity(input) {
59
61
  }
60
62
  }
61
63
 
62
- let recommendation;
63
64
  let band;
64
65
  if (weightedScore >= 3.75) band = "go";
65
66
  else if (weightedScore >= 2.5) band = "rework-required";
@@ -67,14 +68,14 @@ export function scoreOpportunity(input) {
67
68
 
68
69
  const hasStructuralBlocker = blockers.some((b) => STRUCTURAL_BLOCKERS.has(b));
69
70
  const hasMitigableBlocker = blockers.some((b) => MITIGABLE_BLOCKERS.has(b));
71
+ // 0 means "not assessed / insufficient evidence" (feasibility-scoring.md), never a real low score.
72
+ const notAssessed = Object.entries(scores).filter(([, v]) => v === 0).map(([k]) => k);
70
73
 
71
- if (hasStructuralBlocker) {
72
- recommendation = "blocked";
73
- } else if (hasMitigableBlocker) {
74
- recommendation = "rework-required";
75
- } else {
76
- recommendation = band;
77
- }
74
+ let recommendation;
75
+ if (hasStructuralBlocker) recommendation = "blocked";
76
+ else if (notAssessed.length) recommendation = "insufficient-evidence";
77
+ else if (hasMitigableBlocker) recommendation = "rework-required";
78
+ else recommendation = band;
78
79
 
79
80
  return {
80
81
  scores,
@@ -82,18 +83,34 @@ export function scoreOpportunity(input) {
82
83
  band,
83
84
  blockers,
84
85
  recommendation,
85
- blockerOverrodeScore: recommendation !== band,
86
+ notAssessed,
87
+ blockerOverrodeScore: hasStructuralBlocker || (hasMitigableBlocker && recommendation !== band),
88
+ explanation: hasStructuralBlocker
89
+ ? "Structural blocker present: recommendation forced to 'blocked' regardless of score."
90
+ : notAssessed.length
91
+ ? `Not assessed (score 0): ${notAssessed.join(", ")}. Gather evidence before deciding; the weighted score is provisional.`
92
+ : hasMitigableBlocker
93
+ ? "Mitigable blocker present: recommendation forced to 'rework-required' until the mitigation is in place."
94
+ : `Weighted score ${weightedScore} falls in band '${band}'.`,
86
95
  };
87
96
  }
88
97
 
89
98
  function main() {
90
99
  const arg = process.argv[2];
91
100
  if (!arg) {
92
- console.error("Usage: node score-opportunity.mjs '<json-input>'");
101
+ console.error("Usage: node score-opportunity.mjs '<json-input>' (or a path to a .json file)");
102
+ process.exit(1);
103
+ }
104
+ try {
105
+ const text = /^[\[{]/.test(arg.trim()) ? arg : readFileSync(arg, "utf8");
106
+ const input = JSON.parse(text);
107
+ const list = Array.isArray(input) ? input : [input];
108
+ const out = list.map((o) => ({ ...(o.id ? { id: o.id } : {}), ...scoreOpportunity(o) }));
109
+ console.log(JSON.stringify(Array.isArray(input) ? out : out[0], null, 2));
110
+ } catch (err) {
111
+ console.error(`score-opportunity: ${err.message}`);
93
112
  process.exit(1);
94
113
  }
95
- const input = JSON.parse(arg);
96
- console.log(JSON.stringify(scoreOpportunity(input), null, 2));
97
114
  }
98
115
 
99
116
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -1,12 +1,6 @@
1
1
  ---
2
2
  name: presentation-compliance-validator
3
- description: >-
4
- Convert configurable, user-supplied presentation guidelines into structured rules and validate a
5
- generated slide deck against them, reporting specific violations and corrective actions rather than
6
- a vague score. Use this skill after corporate-presentation-generator produces a draft, or whenever
7
- a user wants to check an existing deck against their own corporate guidelines. Never hardcodes a
8
- specific organization's proprietary rules; always reads rules from a user-supplied or default
9
- configuration.
3
+ description: "Convert configurable, user-supplied presentation guidelines into structured rules and validate a generated slide deck against them, reporting specific violations and corrective actions rather than a vague score. Use this skill after corporate-presentation-generator produces a draft, or whenever a user wants to check an existing deck against their own corporate guidelines. Never hardcodes a specific organization's proprietary rules; always reads rules from a user-supplied or default configuration."
10
4
  metadata:
11
5
  version: 1.0.0
12
6
  partOf: enterprise-project-intelligence-agent
@@ -33,14 +27,18 @@ separating what was checked automatically from what needs human judgment.
33
27
 
34
28
  ## Execution procedure
35
29
 
36
- 1. **Load the rule set.** Split rules into `explicit` (directly from the supplied config),
37
- `derived` (a reasonable interpretation the tool computed, e.g., "max bullets" derived from a
38
- stated "content density" guideline), `optional-recommendation`, and `human-review-required`
39
- (anything semantic/visual that cannot be deterministically checked, e.g., "diagram looks
40
- professional").
41
- 2. **Run deterministic checks** with `scripts/validate-deck.mjs`: required sections present, bullet
42
- count per slide, title presence, footer/page-number presence, citation markers present on slides
43
- with numeric claims, font/color values matching the declared palette when the format exposes them.
30
+ 1. **Build or load the rule set.** If the user supplied prose guidelines (a brand guide, a style
31
+ PDF exported to text), convert them with the procedure in `references/rule-schema.md`
32
+ §"Converting a user's prose guideline into a rule set": every statement goes into exactly one
33
+ bucket — `explicit` (measurable, stated), `derived` (qualitative, interpreted; record it in
34
+ `derivedRules`), `optionalRecommendations`, or `humanReviewOnly` (semantic/visual). If a JSON
35
+ rule set was supplied, use it as-is.
36
+ 2. **Run deterministic checks** with
37
+ `node scripts/validate-deck.mjs <deck.json> <rules.json>`. The script first validates the rule
38
+ set; if it returns `schemaErrors`, report them and stop. Otherwise it checks required sections,
39
+ bullet count, titles, footer/page numbers, citations on numeric claims, and palette/typography
40
+ values when the deck exposes them. If you only have a Markdown outline, convert it first with
41
+ `skills/corporate-presentation-generator/scripts/outline-to-deck-json.mjs`.
44
42
  3. **Flag semantic/visual items** for human review rather than guessing a pass/fail (e.g., actual
45
43
  visual balance, logo placement in a rendered image, diagram aesthetic quality).
46
44
  4. **Report specific violations**: rule ID, slide number, what was found, what was expected, and a
@@ -51,6 +49,8 @@ separating what was checked automatically from what needs human judgment.
51
49
  ## Expected outputs
52
50
 
53
51
  - A compliance report listing: rule ID, severity, slide reference, finding, corrective action.
52
+ - When guidelines were converted: the rule-set JSON plus a table showing each guideline statement →
53
+ bucket (explicit / derived / optional / human-review).
54
54
  - A human-review list for semantic/visual items.
55
55
  - An overall pass/fail only for rules marked `blocking` in the rule set; non-blocking rules are
56
56
  reported as recommendations.
@@ -3,22 +3,79 @@
3
3
  ```json
4
4
  {
5
5
  "name": "string - name of the guideline set (user-supplied)",
6
- "requiredSections": ["string, e.g. Executive Summary", "Risks and Decisions"],
6
+ "source": "optional - where the rules came from, e.g. 'brand-guide.pdf §3' (no proprietary text)",
7
+ "requiredSections": ["Executive Summary", "Risks and Decisions"],
7
8
  "maxBulletsPerSlide": 6,
8
9
  "requireTitleOnEverySlide": true,
9
10
  "requireFooterOrPageNumber": true,
10
11
  "requireCitationOnNumericClaims": true,
11
12
  "palette": { "primary": "#RRGGBB", "text": "#RRGGBB", "background": "#RRGGBB", "risk": "#RRGGBB" },
12
13
  "typography": { "titleFont": "string", "bodyFont": "string" },
13
- "blockingRuleIds": ["required-sections", "max-bullets"],
14
- "humanReviewOnly": ["visual-balance", "diagram-aesthetics", "logo-placement"]
14
+ "blockingRuleIds": ["required-sections", "max-bullets", "require-title"],
15
+ "humanReviewOnly": ["visual-balance", "diagram-aesthetics", "logo-placement"],
16
+ "derivedRules": [{ "rule": "maxBulletsPerSlide", "from": "guideline text: 'keep slides sparse'", "interpretation": "6 bullets" }],
17
+ "optionalRecommendations": ["Prefer one key idea per slide"]
15
18
  }
16
19
  ```
17
20
 
18
- - `requiredSections`, `maxBulletsPerSlide`, `requireTitleOnEverySlide`, `requireFooterOrPageNumber`,
19
- and `requireCitationOnNumericClaims` are deterministically checkable and should be in
20
- `blockingRuleIds` if the organization wants them enforced as hard failures.
21
- - `palette`/`typography` are checkable only when the deck format exposes those properties; otherwise
22
- the validator reports `not-checkable-from-this-format`.
23
- - `humanReviewOnly` entries are never auto-passed or auto-failed; they always appear in the
24
- human-review list.
21
+ ## Field types (validated by `scripts/validate-deck.mjs` before any check runs)
22
+
23
+ | Key | Type | Notes |
24
+ |---|---|---|
25
+ | `requiredSections` | array of non-empty strings | Matched case-insensitively against `deck.sections` **and** slide titles (a title may continue after `:`, `—`, or ` -`). |
26
+ | `maxBulletsPerSlide` | positive integer | |
27
+ | `requireTitleOnEverySlide`, `requireFooterOrPageNumber`, `requireCitationOnNumericClaims` | boolean | |
28
+ | `palette`, `typography` | object of strings | Palette values must be `#RRGGBB`. |
29
+ | `blockingRuleIds` | array of known rule ids (below) | Unknown ids are a schema error. |
30
+ | `humanReviewOnly` | array of strings | Free-form; never auto-passed. |
31
+ | `name`, `source`, `notes`, `derivedRules`, `optionalRecommendations` | informational | Not enforced. |
32
+
33
+ Any other key, or a wrong type, makes the rule set **invalid**: the validator returns
34
+ `{ "pass": false, "schemaErrors": [...], "stopped": "invalid rule set" }` and checks nothing.
35
+ Fix the rule set rather than guessing intent.
36
+
37
+ ## Rule IDs emitted by the validator (use these in `blockingRuleIds`)
38
+
39
+ | Rule id | Triggered by | Scope |
40
+ |---|---|---|
41
+ | `required-sections` | a `requiredSections` entry not found | deck |
42
+ | `require-title` | `requireTitleOnEverySlide` and a slide without `title` | slide |
43
+ | `max-bullets` | a slide with more than `maxBulletsPerSlide` bullets | slide |
44
+ | `require-footer` | `requireFooterOrPageNumber` and `hasFooterOrPageNumber` false | slide |
45
+ | `citation-on-numeric-claim` | `requireCitationOnNumericClaims` and `hasNumericClaim` without `citationsPresent` | slide |
46
+ | `palette-<key>` / `typography-<key>` | deck exposes a different value than the rule | deck |
47
+ | `palette-not-checkable` / `typography-not-checkable` | the deck format exposes no such metadata (severity `info`) | deck |
48
+
49
+ Non-blocking findings are reported as `recommendation`; `pass` is false only when a blocking rule fails.
50
+
51
+ ## Deck JSON (validator input)
52
+
53
+ ```json
54
+ {
55
+ "sections": ["Executive Summary", "Risks and Decisions"],
56
+ "palette": { "primary": "#2B6CB0" },
57
+ "slides": [
58
+ { "title": "Executive Summary", "bullets": ["..."], "notes": "...",
59
+ "hasFooterOrPageNumber": true, "hasNumericClaim": false, "citationsPresent": true }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ `corporate-presentation-generator` produces this file from its Markdown outline with
65
+ `skills/corporate-presentation-generator/scripts/outline-to-deck-json.mjs`.
66
+
67
+ ## Converting a user's prose guideline into a rule set
68
+
69
+ 1. Read the user-supplied guideline (never bundle it in this toolkit). For every statement decide
70
+ one bucket:
71
+ - **explicit** — measurable and stated (e.g. "no more than five bullets") → set the field directly;
72
+ - **derived** — stated qualitatively ("keep slides sparse") → set the field AND record the
73
+ interpretation in `derivedRules` so a human can confirm it;
74
+ - **optional recommendation** — stylistic advice → `optionalRecommendations`;
75
+ - **human review** — semantic or visual ("logo must breathe", "diagrams look premium") →
76
+ `humanReviewOnly`. Never convert these into automated pass/fail rules.
77
+ 2. Put a rule in `blockingRuleIds` only if the guideline makes it mandatory ("must", "required").
78
+ 3. Do not invent rules the guideline does not state; leave fields out instead.
79
+ 4. Record provenance in `source` (section numbers, not copied proprietary wording).
80
+ 5. Validate the result: `node scripts/validate-deck.mjs <deck.json> <rules.json>` reports schema errors
81
+ before anything else.