tadforce 0.0.0-stage → 0.3.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 (141) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +202 -0
  4. package/NOTICE +11 -0
  5. package/README.md +266 -2
  6. package/assets/logo-mark.svg +24 -0
  7. package/assets/logo.svg +31 -0
  8. package/bin/tadforce.js +4 -0
  9. package/commands/tadforce-review.md +8 -0
  10. package/commands/tadforce-update.md +6 -0
  11. package/commands/tadforce.md +10 -0
  12. package/package.json +60 -4
  13. package/skills/tadforce/SKILL.md +73 -0
  14. package/skills/tadforce/references/axes-rules.md +29 -0
  15. package/skills/tadforce/references/evidence-rules.md +40 -0
  16. package/skills/tadforce/references/exclusion-heuristics.md +40 -0
  17. package/skills/tadforce/references/figure-rules.md +42 -0
  18. package/skills/tadforce/references/markdown-conventions.md +67 -0
  19. package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
  20. package/skills/tadforce/references/modules/analytics.md +19 -0
  21. package/skills/tadforce/references/modules/commerce.md +25 -0
  22. package/skills/tadforce/references/modules/data-cloud.md +23 -0
  23. package/skills/tadforce/references/modules/experience-cloud.md +24 -0
  24. package/skills/tadforce/references/modules/field-service.md +25 -0
  25. package/skills/tadforce/references/modules/industries.md +21 -0
  26. package/skills/tadforce/references/modules/integration-platforms.md +19 -0
  27. package/skills/tadforce/references/modules/marketing.md +24 -0
  28. package/skills/tadforce/references/modules/omnistudio.md +24 -0
  29. package/skills/tadforce/references/modules/platform-events.md +19 -0
  30. package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
  31. package/skills/tadforce/references/modules/sales-cloud.md +26 -0
  32. package/skills/tadforce/references/modules/security-shield.md +18 -0
  33. package/skills/tadforce/references/modules/service-cloud.md +27 -0
  34. package/skills/tadforce/references/naming-external-systems.md +32 -0
  35. package/skills/tadforce/references/questions.md +59 -0
  36. package/skills/tadforce/references/salesforce-skills.md +27 -0
  37. package/skills/tadforce/references/workflow.md +95 -0
  38. package/skills/tadforce/references/writing-rules.md +47 -0
  39. package/skills/tadforce-review/SKILL.md +53 -0
  40. package/skills/tadforce-update/SKILL.md +48 -0
  41. package/src/build/brand.js +107 -0
  42. package/src/build/browser.js +70 -0
  43. package/src/build/directives.js +293 -0
  44. package/src/build/export.js +60 -0
  45. package/src/build/figures.js +119 -0
  46. package/src/build/mermaid.js +107 -0
  47. package/src/build/render.js +299 -0
  48. package/src/build/serve.js +59 -0
  49. package/src/cli.js +68 -0
  50. package/src/commands/docs.js +204 -0
  51. package/src/commands/doctor.js +370 -0
  52. package/src/commands/init.js +132 -0
  53. package/src/commands/org.js +151 -0
  54. package/src/commands/review.js +35 -0
  55. package/src/commands/spec.js +91 -0
  56. package/src/commands/update.js +154 -0
  57. package/src/facts/extractors/apex.js +111 -0
  58. package/src/facts/extractors/automation.js +85 -0
  59. package/src/facts/extractors/data.js +94 -0
  60. package/src/facts/extractors/integration.js +76 -0
  61. package/src/facts/extractors/misc.js +100 -0
  62. package/src/facts/extractors/security.js +92 -0
  63. package/src/facts/extractors/ui.js +120 -0
  64. package/src/facts/fingerprints.js +32 -0
  65. package/src/facts/heuristics/exclusions.js +143 -0
  66. package/src/facts/heuristics/modules.js +85 -0
  67. package/src/facts/heuristics/naming.js +31 -0
  68. package/src/facts/heuristics/signals.js +76 -0
  69. package/src/facts/index.js +197 -0
  70. package/src/facts/org.js +87 -0
  71. package/src/facts/xml.js +85 -0
  72. package/src/sf/sf.js +120 -0
  73. package/src/util/args.js +21 -0
  74. package/src/util/config.js +104 -0
  75. package/src/util/fsx.js +58 -0
  76. package/src/util/i18n.js +49 -0
  77. package/src/util/log.js +31 -0
  78. package/src/util/paths.js +11 -0
  79. package/src/validate/index.js +126 -0
  80. package/templates/axes/en/00-document-control.md +3 -0
  81. package/templates/axes/en/01-summary.md +7 -0
  82. package/templates/axes/en/02-method.md +9 -0
  83. package/templates/axes/en/03-findings.md +19 -0
  84. package/templates/axes/en/04-register.md +3 -0
  85. package/templates/axes/en/05-roadmap.md +3 -0
  86. package/templates/axes/en/99-appendix.md +5 -0
  87. package/templates/axes/fr/00-document-control.md +3 -0
  88. package/templates/axes/fr/01-summary.md +7 -0
  89. package/templates/axes/fr/02-method.md +9 -0
  90. package/templates/axes/fr/03-findings.md +19 -0
  91. package/templates/axes/fr/04-register.md +3 -0
  92. package/templates/axes/fr/05-roadmap.md +3 -0
  93. package/templates/axes/fr/99-appendix.md +5 -0
  94. package/templates/html/cover.html +12 -0
  95. package/templates/html/editor.js +502 -0
  96. package/templates/html/style.css +255 -0
  97. package/templates/html/template.html +88 -0
  98. package/templates/i18n/en.json +366 -0
  99. package/templates/i18n/fr.json +366 -0
  100. package/templates/manifest/package-tad.xml +543 -0
  101. package/templates/specs/sections.yml +494 -0
  102. package/templates/tad/en/00-document-control.md +7 -0
  103. package/templates/tad/en/01-summary.md +17 -0
  104. package/templates/tad/en/02-purpose.md +35 -0
  105. package/templates/tad/en/03-context.md +17 -0
  106. package/templates/tad/en/04-general.md +29 -0
  107. package/templates/tad/en/05-functional.md +7 -0
  108. package/templates/tad/en/06-data.md +71 -0
  109. package/templates/tad/en/07-application.md +49 -0
  110. package/templates/tad/en/08-ui.md +41 -0
  111. package/templates/tad/en/09-integrations.md +53 -0
  112. package/templates/tad/en/10-security.md +59 -0
  113. package/templates/tad/en/11-reporting.md +13 -0
  114. package/templates/tad/en/12-operations.md +25 -0
  115. package/templates/tad/en/13-nfr.md +9 -0
  116. package/templates/tad/en/14-decisions.md +3 -0
  117. package/templates/tad/en/15-outlook.md +3 -0
  118. package/templates/tad/en/99-appendices.md +119 -0
  119. package/templates/tad/fr/00-document-control.md +7 -0
  120. package/templates/tad/fr/01-summary.md +17 -0
  121. package/templates/tad/fr/02-purpose.md +35 -0
  122. package/templates/tad/fr/03-context.md +17 -0
  123. package/templates/tad/fr/04-general.md +29 -0
  124. package/templates/tad/fr/05-functional.md +7 -0
  125. package/templates/tad/fr/06-data.md +71 -0
  126. package/templates/tad/fr/07-application.md +49 -0
  127. package/templates/tad/fr/08-ui.md +41 -0
  128. package/templates/tad/fr/09-integrations.md +53 -0
  129. package/templates/tad/fr/10-security.md +59 -0
  130. package/templates/tad/fr/11-reporting.md +13 -0
  131. package/templates/tad/fr/12-operations.md +25 -0
  132. package/templates/tad/fr/13-nfr.md +9 -0
  133. package/templates/tad/fr/14-decisions.md +3 -0
  134. package/templates/tad/fr/15-outlook.md +3 -0
  135. package/templates/tad/fr/99-appendices.md +119 -0
  136. package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
  137. package/templates/vendor/LICENSE-html2canvas.txt +22 -0
  138. package/templates/vendor/LICENSE-mermaid.txt +21 -0
  139. package/templates/vendor/html-docx.js +13216 -0
  140. package/templates/vendor/html2canvas.min.js +20 -0
  141. package/templates/vendor/mermaid.min.js +2314 -0
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+
5
+ export const exists = (p) => fs.existsSync(p);
6
+ // BOM removed and line endings normalised: sources edited on Windows (CRLF) behave like the others.
7
+ export const readText = (p) => fs.readFileSync(p, 'utf8').replace(/^/, '').replace(/\r\n?/g, '\n');
8
+ export const readJson = (p) => JSON.parse(readText(p));
9
+ export const safeReadJson = (p) => { try { return readJson(p); } catch { return null; } };
10
+ export const readYaml =(p, fallback = {}) => (exists(p) ? YAML.parse(readText(p)) ?? fallback : fallback);
11
+
12
+ export function writeText(p, s) {
13
+ fs.mkdirSync(path.dirname(p), { recursive: true });
14
+ fs.writeFileSync(p, s, 'utf8');
15
+ }
16
+
17
+ /** Deterministic JSON: object keys sorted, two-space indent, trailing newline. */
18
+ export function stableStringify(value) {
19
+ const sort = (v) => {
20
+ if (Array.isArray(v)) return v.map(sort);
21
+ if (v && typeof v === 'object') {
22
+ return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])]));
23
+ }
24
+ return v;
25
+ };
26
+ return JSON.stringify(sort(value), null, 2) + '\n';
27
+ }
28
+
29
+ export const writeJson = (p, v) => writeText(p, stableStringify(v));
30
+
31
+ /** Recursively list files under dir (sorted, forward-slash relative paths). */
32
+ export function walk(dir, { filter = () => true } = {}) {
33
+ const out = [];
34
+ if (!exists(dir)) return out;
35
+ const visit = (d) => {
36
+ for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
37
+ const full = path.join(d, e.name);
38
+ if (e.isDirectory()) {
39
+ if (e.name === 'node_modules' || e.name.startsWith('.git')) continue;
40
+ visit(full);
41
+ } else if (filter(full)) out.push(full);
42
+ }
43
+ };
44
+ visit(dir);
45
+ return out;
46
+ }
47
+
48
+ export const toPosix = (p) => p.split(path.sep).join('/');
49
+
50
+ export function copyDir(src, dst, { overwrite = false } = {}) {
51
+ for (const f of walk(src)) {
52
+ const rel = path.relative(src, f);
53
+ const target = path.join(dst, rel);
54
+ if (!overwrite && exists(target)) continue;
55
+ fs.mkdirSync(path.dirname(target), { recursive: true });
56
+ fs.copyFileSync(f, target);
57
+ }
58
+ }
@@ -0,0 +1,49 @@
1
+ import path from 'node:path';
2
+ import { readJson, exists } from './fsx.js';
3
+ import { TEMPLATES } from './paths.js';
4
+
5
+ /** Languages shipped with skeletons and interface labels. Any other language works from the English ones. */
6
+ export const LANGUAGES = ['en', 'fr'];
7
+
8
+ /** A language code as written in the config: en, fr, es, de, pt-BR… (anything else falls back to en). */
9
+ export function normalizeLang(lang) {
10
+ const m = /^([a-z]{2,3})(?:[-_]([a-z]{2}))?$/i.exec(String(lang || '').trim());
11
+ return m ? m[1].toLowerCase() + (m[2] ? '-' + m[2].toUpperCase() : '') : 'en';
12
+ }
13
+
14
+ /** Skeletons and specs come in the built-in languages; other languages start from English. */
15
+ export const templateLang = (lang) => (LANGUAGES.includes(normalizeLang(lang)) ? normalizeLang(lang) : 'en');
16
+
17
+ /**
18
+ * Interface and document labels: English, then the built-in translation, then the workspace's own
19
+ * file (tadforce/i18n/<lang>.json) — the translation the agent writes for any other language.
20
+ */
21
+ export function loadI18n(lang, ws = null) {
22
+ const l = normalizeLang(lang);
23
+ let dict = readJson(path.join(TEMPLATES, 'i18n', 'en.json'));
24
+ const builtIn = path.join(TEMPLATES, 'i18n', `${l}.json`);
25
+ if (l !== 'en' && exists(builtIn)) dict = merge(dict, readJson(builtIn));
26
+ const own = ws && path.join(ws, 'i18n', `${l}.json`);
27
+ if (own && exists(own)) dict = merge(dict, readJson(own));
28
+ return { lang: l, dict };
29
+ }
30
+
31
+ function merge(a, b) {
32
+ const out = { ...a };
33
+ for (const [k, v] of Object.entries(b)) out[k] = v && typeof v === 'object' && !Array.isArray(v) ? merge(a[k] || {}, v) : v;
34
+ return out;
35
+ }
36
+
37
+ /** t(dict, 'annex.name', {n: 3}) → string with {n} replaced; falls back to the key. */
38
+ export function t(dict, key, vars = {}) {
39
+ const val = key.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), dict);
40
+ const s = typeof val === 'string' ? val : key;
41
+ return s.replace(/\{(\w+)\}/g, (m, k) => (vars[k] !== undefined ? String(vars[k]) : m));
42
+ }
43
+
44
+ export function formatNumber(n, lang) {
45
+ if (typeof n !== 'number' || !Number.isFinite(n)) return String(n);
46
+ // fr-FR uses a narrow no-break space as thousands separator, as in the reference document.
47
+ const locale = lang === 'fr' ? 'fr-FR' : lang === 'en' || !lang ? 'en-US' : lang;
48
+ try { return new Intl.NumberFormat(locale).format(n); } catch { return new Intl.NumberFormat('en-US').format(n); }
49
+ }
@@ -0,0 +1,31 @@
1
+ // Terminal output with the TADForce voice: short, precise, evidence-first.
2
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
3
+ const paint = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
4
+
5
+ export const c = {
6
+ bold: paint('1'),
7
+ dim: paint('2'),
8
+ blue: paint('34'),
9
+ violet: paint('35'),
10
+ green: paint('32'),
11
+ yellow: paint('33'),
12
+ red: paint('31'),
13
+ cyan: paint('36'),
14
+ };
15
+
16
+ let quiet = false;
17
+ export function setQuiet(v) { quiet = v; }
18
+
19
+ export const log = {
20
+ banner(sub) {
21
+ if (quiet) return;
22
+ console.log(`${c.violet('▲')} ${c.bold('TADForce')} ${c.dim('· no evidence, no sentence')}${sub ? c.dim(' · ' + sub) : ''}`);
23
+ },
24
+ step(msg) { if (!quiet) console.log(`${c.blue('›')} ${msg}`); },
25
+ ok(msg) { if (!quiet) console.log(`${c.green('✔')} ${msg}`); },
26
+ warn(msg) { if (!quiet) console.log(`${c.yellow('⚠')} ${msg}`); },
27
+ fail(msg) { console.error(`${c.red('✖')} ${msg}`); },
28
+ info(msg) { if (!quiet) console.log(` ${c.dim(msg)}`); },
29
+ human(msg) { if (!quiet) console.log(`${c.cyan('☞')} ${c.bold('Your move:')} ${msg}`); },
30
+ plain(msg) { if (!quiet) console.log(msg); },
31
+ };
@@ -0,0 +1,11 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ /** Root of the installed TADForce package (where templates/ and skills/ live). */
5
+ export const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
6
+ export const TEMPLATES = path.join(PKG_ROOT, 'templates');
7
+ export const SKILLS = path.join(PKG_ROOT, 'skills');
8
+
9
+ /** Default name of the workspace folder created inside the Salesforce project. */
10
+ export const WORKSPACE_DIR = 'tadforce';
11
+ export const CONFIG_FILE = 'tadforce.config.json';
@@ -0,0 +1,126 @@
1
+ // `tadforce validate`: the gate between a draft and a deliverable.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { exists, readText, readYaml } from '../util/fsx.js';
5
+ import { TEMPLATES } from '../util/paths.js';
6
+ import { loadFacts } from '../facts/index.js';
7
+ import { readComments } from '../build/render.js';
8
+
9
+ // Identifiers that look like metadata names but are Salesforce vocabulary, not components.
10
+ const COMMON = new Set(('Account Contact Lead Opportunity Case Task Event User Group Campaign CampaignMember Asset Contract Order Product2 Pricebook2 Individual Profile PermissionSet ' +
11
+ 'RecordType ContentDocument ContentVersion Attachment Note FeedItem EmailMessage OpportunityLineItem Quote AccountContactRelation Organization Id Name OwnerId CreatedDate ' +
12
+ 'LastModifiedDate RecordTypeId Status Type ApexClass ApexTrigger Flow FlowDefinition LWC Aura Visualforce Batchable Queueable Schedulable Database Stateful HttpRequest ' +
13
+ 'HttpResponse Http AuraEnabled InvocableMethod RestResource future isTest with without inherited sharing USER_MODE SECURITY_ENFORCED SYSTEM_MODE true false null ' +
14
+ 'before after insert update delete undelete Trigger System Schema Test Limits Messaging Platform Event Lookup MasterDetail Hierarchy Picklist Text Number Date DateTime ' +
15
+ 'Checkbox Formula Currency Percent Email Phone Url TextArea LongTextArea Html Master Detail Private Public Read ReadWrite ControlledByParent FullAccess None All ' +
16
+ 'RecordPage AppPage HomePage UtilityBar Console Standard Large Small lightning__RecordPage lightning__AppPage lightning__HomePage lightning__FlowScreen lightning__Tab ' +
17
+ 'Active Draft Obsolete Inactive OAuth JWT SAML OpenIdConnect REST SOAP JSON XML CSV HTTPS HTTP GET POST PUT PATCH DELETE SOQL SOSL DML API CRUD FLS OWD PSG CMDT CDC ' +
18
+ 'Named External Credential Credentials Remote Site Settings CSP CORS SSO MFA IP UI ID ' +
19
+ // standard objects, fields and Apex types frequently quoted in architecture documents
20
+ 'ContentDocumentLink ContentWorkspace ContentNote EmailTemplate OrgWideEmailAddress PermissionSetAssignment PermissionSetGroup GroupMember UserRole UserLicense ' +
21
+ 'FederationIdentifier ProfileId UserRoleId IsActive IsConverted ConvertedAccountId ConvertedOpportunityId WhatId WhoId ParentId ActivityDate StageName CloseDate ' +
22
+ 'AccountShare OpportunityShare LeadShare CaseShare ServiceAppointment WorkOrder AgentWork PendingServiceRouting ServiceChannel QueueRoutingConfig PresenceUserConfig ' +
23
+ 'AuraHandledException DmlException QueryException CalloutException HttpCalloutMock StubProvider WebServiceMock Callable Comparable Iterable Iterator ' +
24
+ 'TriggerOperation EventBus QueueableContext SchedulableContext BatchableContext RestContext RestRequest RestResponse JSONGenerator JSONParser Crypto EncodingUtil ' +
25
+ 'ScheduledJob CronTrigger AsyncApexJob ApexClass ApexPage FlowInterview PlatformEventSubscriberConfig OmniProcess OmniDataTransform OmniUiCard').split(/\s+/));
26
+
27
+ const SECRET_PATTERNS = [
28
+ ['private-key', /-----BEGIN (RSA |EC |)PRIVATE KEY-----/],
29
+ ['bearer-token', /\bBearer\s+[A-Za-z0-9\-._~+/]{20,}/],
30
+ ['sf-session-id', /\b00D[A-Za-z0-9]{12,15}![A-Za-z0-9._]{20,}/],
31
+ ['org-id', /\b00D[A-Za-z0-9]{12}(?:[A-Za-z0-9]{3})?\b/],
32
+ ['client-secret', /client[_-]?secret\s*[:=]\s*['"]?[A-Za-z0-9]{12,}/i],
33
+ ['password', /\bpassword\s*[:=]\s*['"]?[^\s'"]{6,}/i],
34
+ ['api-key', /\b(sk|pk)_(live|test)_[A-Za-z0-9]{16,}/],
35
+ ];
36
+
37
+ const COUNT_WORDS = /\b(\d{1,3}(?:[ ,.  ]\d{3})*|\d+)\s+(?:active\s+|actives?\s+)?(apex classes|classes apex|classes|triggers?|flows?|lwc|lightning web components|objects?|objets?|custom objects|permission sets?|permission set groups?|profiles?|profils?|roles?|rôles?|queues?|named credentials?|integration procedures?|omniscripts?|data mappers?|record types?|custom fields?|champs)\b/gi;
38
+
39
+ /**
40
+ * Validate the Markdown sources of a document (and its last build output when present).
41
+ * Returns { errors, warnings, score }.
42
+ */
43
+ export function validateDocument(wsCtx, doc, { buildReport = null } = {}) {
44
+ const issues = [];
45
+ const add = (level, codeId, message, where) => issues.push({ level, code: codeId, message, where });
46
+ const facts = loadFacts(wsCtx.ws);
47
+ if (!facts) { add('error', 'no-facts', 'facts/facts.json is missing: run `tadforce facts`.'); return summarize(issues); }
48
+ const srcDir = wsCtx.p('src', doc);
49
+ if (!exists(srcDir)) { add('error', 'no-sources', `src/${doc} is missing: run \`tadforce scaffold --doc ${doc}\`.`); return summarize(issues); }
50
+ const files = fs.readdirSync(srcDir).filter((f) => f.endsWith('.md')).sort();
51
+ const glossary = wsCtx.glossary() || {};
52
+ const knowledge = wsCtx.exclusions() || {};
53
+ // glossary.yml › allow: technical terms quoted on purpose (API payload fields, external identifiers…)
54
+ const inventory = new Set([...facts.inventory, ...((glossary.allow || []).map(String))]);
55
+ const excluded = new Set((facts.exclusions.applied || []).map((x) => x.name));
56
+ const avoid = (glossary.terms || []).flatMap((x) => (x.avoid || []).map((a) => ({ avoid: a, use: x.use || x.term })));
57
+ const forbiddenFeatures = (knowledge.features || []).flatMap((f) => (f.keywords || []).map((k) => ({ k, feature: f.name })));
58
+
59
+ // required sections of the skeleton must still exist (by explicit id)
60
+ const specs = readYaml(path.join(TEMPLATES, 'specs', 'sections.yml'), {});
61
+ const all = files.map((f) => ({ f, text: readText(path.join(srcDir, f)) }));
62
+ const joined = all.map((x) => x.text).join('\n');
63
+ for (const anchor of (specs.requiredAnchors && specs.requiredAnchors[doc]) || []) {
64
+ if (!new RegExp(`\\{[^}]*#${anchor}[\\s}]`).test(joined)) add('error', 'missing-section', `Required section #${anchor} was removed from the skeleton.`);
65
+ }
66
+
67
+ for (const { f, text } of all) {
68
+ const lines = text.split('\n');
69
+ let inCode = false;
70
+ lines.forEach((line, i) => {
71
+ const where = `src/${doc}/${f}:${i + 1}`;
72
+ if (line.startsWith('```')) inCode = !inCode;
73
+ const pend = /<!--\s*tad:pending\s+([\w.-]+)\s*-->/.exec(line);
74
+ // sections inside a false <!-- tad:if --> block are not part of this org's document
75
+ if (pend && (!buildReport || buildReport.pending.includes(pend[1]))) add('error', 'pending', `Section "${pend[1]}" is not written yet.`, where);
76
+ if (/\bTODO\b|\bTBD\b|lorem ipsum/i.test(line)) add('error', 'todo', 'Placeholder text left in the document.', where);
77
+ for (const [id, re] of SECRET_PATTERNS) if (re.test(line)) add('error', 'secret', `Possible secret or org identifier (${id}).`, where);
78
+ for (const a of avoid) if (new RegExp(`\\b${escapeRe(a.avoid)}\\b`, 'i').test(line)) add('error', 'terminology', `"${a.avoid}" must be written "${a.use}" (glossary).`, where);
79
+ for (const fk of forbiddenFeatures) if (new RegExp(`\\b${escapeRe(fk.k)}\\b`, 'i').test(line)) add('error', 'decommissioned', `Mentions "${fk.k}" (decommissioned feature: ${fk.feature}).`, where);
80
+ if (inCode || line.trim().startsWith('|') && /^\|[\s|:-]+\|$/.test(line.trim())) return;
81
+ // hard-coded counts next to component words (use {{n:...}} placeholders instead)
82
+ if (!/^\s*```/.test(line)) {
83
+ for (const m of line.replace(/\{\{[^}]+\}\}/g, '').replace(/`[^`]*`/g, '').matchAll(COUNT_WORDS)) {
84
+ if (Number(m[1].replace(/\D/g, '')) >= 2) add('warn', 'hardcoded-count', `"${m[0]}": use a {{n:…}} placeholder so the figure follows the facts.`, where);
85
+ }
86
+ }
87
+ // identifiers in code font must exist in the inventory, and must not be excluded
88
+ for (const m of line.matchAll(/`([^`\s]{3,80})`/g)) {
89
+ const id = m[1].replace(/\(\)$/, '');
90
+ if (excluded.has(id)) { add('error', 'excluded-component', `\`${id}\` is excluded (knowledge/exclusions.yml) and must not be documented.`, where); continue; }
91
+ if (/[*_]$/.test(id) || /^\*/.test(id)) continue; // naming patterns such as `NWB_*` or `*Service`
92
+ if (!looksLikeComponent(id) || COMMON.has(id) || inventory.has(id)) continue;
93
+ if (/^[\w]+\.[\w]+$/.test(id) && inventory.has(id.split('.')[1])) continue;
94
+ add('warn', 'unknown-component', `\`${id}\` is not in the metadata inventory (typo, other org, or package component?).`, where);
95
+ }
96
+ });
97
+ if (inCode) add('error', 'unclosed-code', 'Unclosed code fence.', `src/${doc}/${f}`);
98
+ }
99
+
100
+ if (buildReport) {
101
+ for (const p of buildReport.problems) add(p.level === 'warn' ? 'warn' : 'error', p.code, p.message);
102
+ const html = readText(buildReport.html);
103
+ const ids = new Set([...html.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]));
104
+ for (const m of html.matchAll(/href="#([^"]+)"/g)) if (!ids.has(decodeURIComponent(m[1]))) add('error', 'broken-anchor', `Link to #${m[1]} has no target.`);
105
+ const open = readComments(html).filter((c) => c.status !== 'resolved');
106
+ if (open.length) add('warn', 'open-comments', `${open.length} review comment(s) still open.`);
107
+ }
108
+ return summarize(issues);
109
+ }
110
+
111
+ function looksLikeComponent(id) {
112
+ if (/__(c|mdt|e|x|b|r)$/.test(id)) return true;
113
+ if (/^[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]+$/.test(id)) return true;
114
+ if (/^[a-z]+[A-Z][A-Za-z0-9]*$/.test(id) && id.length > 5) return true;
115
+ if (/^[A-Z][a-z0-9]+([A-Z][a-z0-9]+){1,}$/.test(id) && id.length > 8) return true;
116
+ return false;
117
+ }
118
+
119
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
120
+
121
+ function summarize(issues) {
122
+ const errors = issues.filter((i) => i.level === 'error');
123
+ const warnings = issues.filter((i) => i.level !== 'error');
124
+ const score = Math.max(0, 100 - errors.length * 5 - warnings.length);
125
+ return { ok: errors.length === 0, errors, warnings, score };
126
+ }
@@ -0,0 +1,3 @@
1
+ # Document control {- #doc-control}
2
+
3
+ @@ANNEX:doc-control@@
@@ -0,0 +1,7 @@
1
+ # Summary {#ax-summary}
2
+
3
+ <!-- tad:pending axes.summary -->
4
+
5
+ ## Signals at a glance {#ax-overview}
6
+
7
+ @@ANNEX:signals@@
@@ -0,0 +1,9 @@
1
+ # Method {#ax-method}
2
+
3
+ The signals below are **measured** on the metadata of the organization (and, when connected, on aggregated org facts). A signal is an observation, not a verdict: each one is qualified by the architect before it becomes an improvement axis.
4
+
5
+ - **Pillars** follow the Salesforce Well-Architected framework: *Trusted* (security, compliance, reliability), *Easy* (user experience, operations, intentional design) and *Adaptable* (maintainability, scalability, resilience to change).
6
+ - **Priorities**: <span class="b p1">P1</span> act now (risk for security, data or continuity); <span class="b p2">P2</span> plan in the next releases; <span class="b p3">P3</span> improve when touching the area.
7
+ - Each axis has an identifier `AX-nn`, reused in the register and in the roadmap.
8
+
9
+ <!-- tad:pending axes.method -->
@@ -0,0 +1,19 @@
1
+ # Findings {#ax-findings}
2
+
3
+ ## Trusted — security, compliance, reliability {#ax-trusted}
4
+
5
+ @@ANNEX:signals-trusted@@
6
+
7
+ <!-- tad:pending axes.trusted -->
8
+
9
+ ## Easy — user experience and operations {#ax-easy}
10
+
11
+ @@ANNEX:signals-easy@@
12
+
13
+ <!-- tad:pending axes.easy -->
14
+
15
+ ## Adaptable — maintainability and scalability {#ax-adaptable}
16
+
17
+ @@ANNEX:signals-adaptable@@
18
+
19
+ <!-- tad:pending axes.adaptable -->
@@ -0,0 +1,3 @@
1
+ # Improvement register {#ax-register}
2
+
3
+ <!-- tad:pending axes.register -->
@@ -0,0 +1,3 @@
1
+ # Roadmap {#ax-roadmap}
2
+
3
+ <!-- tad:pending axes.roadmap -->
@@ -0,0 +1,5 @@
1
+ # Appendix {- #ax-appendix}
2
+
3
+ ## Components that look unused {- #ax-candidates}
4
+
5
+ @@ANNEX:exclusion-candidates@@
@@ -0,0 +1,3 @@
1
+ # Gestion documentaire {- #doc-control}
2
+
3
+ @@ANNEX:doc-control@@
@@ -0,0 +1,7 @@
1
+ # Synthèse {#ax-summary}
2
+
3
+ <!-- tad:pending axes.summary -->
4
+
5
+ ## Signaux en un coup d'œil {#ax-overview}
6
+
7
+ @@ANNEX:signals@@
@@ -0,0 +1,9 @@
1
+ # Méthode {#ax-method}
2
+
3
+ Les signaux ci-dessous sont **mesurés** sur les métadonnées de l'organisation (et, en mode connecté, sur des agrégats de l'organisation). Un signal est un constat, pas un verdict : chacun est qualifié par l'architecte avant de devenir un axe d'amélioration.
4
+
5
+ - **Piliers** du cadre Salesforce Well-Architected : *Fiable* (sécurité, conformité, fiabilité), *Simple* (expérience utilisateur, exploitation, conception intentionnelle) et *Évolutif* (maintenabilité, montée en charge, capacité à évoluer).
6
+ - **Priorités** : <span class="b p1">P1</span> agir maintenant (risque pour la sécurité, les données ou la continuité) ; <span class="b p2">P2</span> planifier dans les prochaines versions ; <span class="b p3">P3</span> améliorer à l'occasion d'une évolution du domaine.
7
+ - Chaque axe porte un identifiant `AX-nn`, repris dans le registre et dans la feuille de route.
8
+
9
+ <!-- tad:pending axes.method -->
@@ -0,0 +1,19 @@
1
+ # Constats {#ax-findings}
2
+
3
+ ## Fiable — sécurité, conformité, fiabilité {#ax-trusted}
4
+
5
+ @@ANNEX:signals-trusted@@
6
+
7
+ <!-- tad:pending axes.trusted -->
8
+
9
+ ## Simple — expérience utilisateur et exploitation {#ax-easy}
10
+
11
+ @@ANNEX:signals-easy@@
12
+
13
+ <!-- tad:pending axes.easy -->
14
+
15
+ ## Évolutif — maintenabilité et montée en charge {#ax-adaptable}
16
+
17
+ @@ANNEX:signals-adaptable@@
18
+
19
+ <!-- tad:pending axes.adaptable -->
@@ -0,0 +1,3 @@
1
+ # Registre des axes d'amélioration {#ax-register}
2
+
3
+ <!-- tad:pending axes.register -->
@@ -0,0 +1,3 @@
1
+ # Feuille de route {#ax-roadmap}
2
+
3
+ <!-- tad:pending axes.roadmap -->
@@ -0,0 +1,5 @@
1
+ # Annexe {- #ax-appendix}
2
+
3
+ ## Composants qui semblent inutilisés {- #ax-candidates}
4
+
5
+ @@ANNEX:exclusion-candidates@@
@@ -0,0 +1,12 @@
1
+ <div class="kicker">{{COVER_KICKER}}</div>
2
+ <h1 class="ctitle">{{DOC_TITLE}}</h1>
3
+ <p class="csub">{{DOC_SUBTITLE}}</p>
4
+ <div class="meta">
5
+ <div><b>{{T:doc.meta.reference}}</b><span>{{DOC_REFERENCE}}</span></div>
6
+ <div><b>{{T:doc.meta.version}}</b><span>{{DOC_VERSION}}</span></div>
7
+ <div><b>{{T:doc.meta.date}}</b><span>{{DOC_DATE}}</span></div>
8
+ {{PREPARED_BY_ROW}}
9
+ <div><b>{{T:doc.meta.recipient}}</b><span>{{CLIENT_NAME}}</span></div>
10
+ <div><b>{{T:doc.meta.classification}}</b><span>{{DOC_CLASSIFICATION}}</span></div>
11
+ </div>
12
+ <p class="conf">{{COVER_NOTE}}</p>