@thanbir-tamim/enterprise-project-intelligence-agent 0.1.0 → 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.
- package/CHANGELOG.md +23 -2
- package/README.md +302 -46
- package/assets/figures/feasibility-scoring-engine.png +0 -0
- package/assets/figures/hero-architecture.png +0 -0
- package/assets/figures/skills-interaction-map.png +0 -0
- package/docs/INSTALLATION.md +6 -6
- package/docs/SUPPORTED_FORMATS.md +4 -4
- package/examples/synthetic-inventory-system/ai-opportunity-matrix/AI_OPPORTUNITY_MATRIX.md +4 -4
- package/examples/synthetic-inventory-system/generated-docs/INVENTORY_SUMMARY.md +17 -0
- package/examples/synthetic-inventory-system/generated-docs/evidence-index.json +78 -0
- package/examples/synthetic-inventory-system/generated-docs/inventory.json +26 -0
- package/examples/synthetic-inventory-system/presentation/DEMO_DECK.md +69 -55
- package/examples/synthetic-inventory-system/presentation/demo-deck.json +194 -16
- package/package.json +2 -1
- package/scripts/cli.mjs +20 -14
- package/scripts/lib/skills.mjs +1 -1
- package/scripts/release-audit.mjs +73 -70
- package/skills/corporate-presentation-generator/SKILL.md +49 -23
- package/skills/corporate-presentation-generator/scripts/outline-to-deck-json.mjs +77 -0
- package/skills/documentation-quality-auditor/SKILL.md +28 -21
- package/skills/documentation-quality-auditor/references/audit-checks.md +15 -8
- package/skills/documentation-quality-auditor/scripts/audit-docs.mjs +192 -32
- package/skills/engineering-knowledge-transfer/SKILL.md +1 -5
- package/skills/enterprise-ai-integration-planner/SKILL.md +13 -15
- package/skills/enterprise-ai-integration-planner/references/feasibility-scoring.md +26 -5
- package/skills/enterprise-ai-integration-planner/scripts/score-opportunity.mjs +32 -15
- package/skills/presentation-compliance-validator/SKILL.md +15 -15
- package/skills/presentation-compliance-validator/references/rule-schema.md +67 -10
- package/skills/presentation-compliance-validator/scripts/validate-deck.mjs +48 -11
- package/skills/project-documentation-discovery/SKILL.md +25 -8
- package/skills/project-documentation-discovery/references/format-matrix.md +4 -4
- package/skills/project-documentation-discovery/scripts/inventory.mjs +174 -100
- package/skills/project-documentation-discovery/scripts/secrets.mjs +99 -0
- package/skills/project-knowledge-assistant/SKILL.md +1 -7
- package/skills/software-change-impact-analyzer/SKILL.md +15 -11
- package/skills/software-reverse-engineering/SKILL.md +14 -10
- package/skills/software-reverse-engineering/references/system-map-schema.md +27 -0
- package/skills/technical-document-generator/SKILL.md +6 -8
- 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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
42
|
-
blocker
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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. **
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|