tadforce 0.3.0 → 0.4.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -5
- package/package.json +1 -1
- package/skills/tadforce/references/questions.md +8 -9
- package/skills/tadforce/references/workflow.md +13 -7
- package/skills/tadforce-review/SKILL.md +2 -0
- package/skills/tadforce-update/SKILL.md +6 -4
- package/src/build/directives.js +16 -11
- package/src/build/figures.js +1 -1
- package/src/build/render.js +8 -5
- package/src/commands/docs.js +34 -13
- package/src/commands/doctor.js +399 -135
- package/src/commands/org.js +75 -11
- package/src/commands/spec.js +19 -6
- package/src/commands/update.js +54 -23
- package/src/facts/extractors/misc.js +2 -0
- package/src/facts/heuristics/exclusions.js +13 -1
- package/src/facts/heuristics/modules.js +5 -3
- package/src/facts/index.js +20 -1
- package/src/sf/sf.js +28 -4
- package/src/util/agents.js +35 -0
- package/src/util/config.js +4 -1
- package/src/util/i18n.js +10 -0
- package/src/validate/index.js +3 -2
- package/templates/html/editor.js +25 -0
- package/templates/i18n/en.json +8 -2
- package/templates/i18n/fr.json +8 -2
- package/templates/manifest/package-tad.xml +4 -104
- package/templates/specs/sections.yml +3 -1
- package/templates/tad/en/02-purpose.md +15 -2
- package/templates/tad/fr/02-purpose.md +15 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "tadforce",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"description": "AI-Powered Salesforce Technical Documentation: generate, review, validate and update the Technical Architecture Document of any Salesforce org (/tadforce, /tadforce-review, /tadforce-update).",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.4.1",
|
|
15
15
|
"author": { "name": "Jihad AZAMI HASSANI" },
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
17
|
"category": "documentation"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tadforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "AI-Powered Salesforce Technical Documentation — the Technical Architecture Document of any Salesforce org, written from the org's own metadata: evidence-based, reproducible, reviewable in the browser, kept up to date. All Salesforce products and industries.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jihad AZAMI HASSANI",
|
package/README.md
CHANGED
|
@@ -47,11 +47,9 @@ From your Salesforce DX project (or an empty folder):
|
|
|
47
47
|
npx tadforce doctor
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
That one command installs everything: it downloads TADForce, installs the agent skills in your project, connects to the org, collects the metadata and asks you the few questions only a human can answer. When everything is green, it says so and **offers to hand over to your AI agent** (Claude Code, Codex, Gemini CLI or Cursor, detected on your machine), which writes, builds and validates the document — nothing else to type.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
/tadforce
|
|
54
|
-
```
|
|
52
|
+
Already in your agent? Type `/tadforce`: it runs the same checks, then writes.
|
|
55
53
|
|
|
56
54
|
<table>
|
|
57
55
|
<tr><th width="33%">A — Claude Code plugin</th><th width="33%">B — Any agent (Agent Skills)</th><th width="33%">C — Deterministic / CI</th></tr>
|
|
@@ -134,7 +132,7 @@ The document carries **the client's logo and colours**: the logo is searched in
|
|
|
134
132
|
|
|
135
133
|
| Your situation | How TADForce handles it |
|
|
136
134
|
|---|---|
|
|
137
|
-
| **A whole org**, as-built | `doctor` retrieves everything with a curated `*` manifest (
|
|
135
|
+
| **A whole org**, as-built | `doctor` retrieves everything with a curated `*` manifest (105 types — only what the document uses, foldered reports and dashboards, standard objects) |
|
|
138
136
|
| **One project in a shared org** (several BUs, several integrators) | Scope: `--scope "NWB_*,Loan*"` or `--package-dirs`. Out-of-scope components leave facts, counts and tables; standard objects and org-wide settings stay |
|
|
139
137
|
| **Before or during the build** | Give your functional specifications (`--specs SFD.pdf,…`): they become evidence (level P2/P3) next to the metadata |
|
|
140
138
|
| **No org access**, sources only | `--source local` documents the package directories of the project |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tadforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "AI-Powered Salesforce Technical Documentation — the Technical Architecture Document of any Salesforce org, written from the org's own metadata: evidence-based, reproducible, reviewable in the browser, kept up to date.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -4,23 +4,22 @@ Ask in **blocks**, each in one message, with your proposed answer for every item
|
|
|
4
4
|
|
|
5
5
|
## Block A — startup (before or right after authorization)
|
|
6
6
|
|
|
7
|
-
`npx tadforce doctor --json`
|
|
7
|
+
`npx tadforce doctor --json` asks, in this order and one step at a time: the **project** (is this the Salesforce project to document?), the **org** (is this the org?), then these framing questions — six, each with a `recommended` answer. Ask them all in one message and let the human accept them all at once:
|
|
8
8
|
|
|
9
9
|
1. Document language — recommended: the org default language. English and French are built in; any other language works (the headings and labels are translated once, see `workflow.md › Other languages`).
|
|
10
|
-
2. Client name
|
|
11
|
-
3. Integrator — **optional**. Blank: the document names the client only (no empty « Integrator » line, no badge).
|
|
10
|
+
2. Client name, as it must appear in the document — recommended: `Organization.Name`. The short code (file names) and the reference are derived from it.
|
|
11
|
+
3. Integrator — **optional**. Blank: the document names the client only (no empty « Integrator » line, no badge). The Improvement Axes then carry the client's brand.
|
|
12
12
|
4. Author — **optional**. Blank: no author is shown anywhere. Never put your own name or the agent's.
|
|
13
|
-
5.
|
|
13
|
+
5. Functional specifications — **optional**: paths to PDF / DOCX / MD files (copied into `knowledge/inputs/specs/`).
|
|
14
14
|
6. Scope — **optional**: name patterns of the components the project owns (`NWB_*,Loan*`), when the org is shared with other teams. Blank: the whole org.
|
|
15
|
-
7. Client logo — **optional**: a file or a URL. Blank: TADForce searches the org (static resources, content assets, site assets), then falls back to an initials badge. The document colours follow the org's Lightning theme, else the logo. If none is found and the client has a public website, you may *offer* to look for the logo there — fetch it only after the human says yes.
|
|
16
|
-
8. Functional specifications — **optional**: paths to PDF / DOCX / MD files (copied into `knowledge/inputs/specs/`).
|
|
17
|
-
9. Metadata — `org` (recommended: retrieve everything from the org), `local` (the sources of this project), or `both`.
|
|
18
15
|
|
|
19
|
-
Answer with `npx tadforce doctor --json --confirm --lang fr --client "…" --
|
|
16
|
+
Answer with `npx tadforce doctor --json --confirm --lang fr --client "…" --integrator none --author none [--specs … | --no-specs] [--scope "…"]`. The title stays « Salesforce CRM Platform » unless the human gives one (`--title`).
|
|
17
|
+
|
|
18
|
+
Then come the metadata choice (`source`, which is also the go for the retrieve), the **branding** (logo and colours found in the org — never fetch a logo from the web without the human's go-ahead), and Blocks B to D.
|
|
20
19
|
|
|
21
20
|
FR example:
|
|
22
21
|
|
|
23
|
-
> Pour démarrer, je propose : langue **français** (langue par défaut de l'org : fr_FR) · client **« Banque Exemple »**
|
|
22
|
+
> Pour démarrer, je propose : langue **français** (langue par défaut de l'org : fr_FR) · client **« Banque Exemple »** · intégrateur : **aucun** · auteur : **aucun** · spécifications fonctionnelles : **aucune** · périmètre : **toute l'org**. Je valide tout ? Indiquez seulement ce qui change.
|
|
24
23
|
|
|
25
24
|
## Block B — external systems (from `facts/questions.json › systems`)
|
|
26
25
|
|
|
@@ -13,27 +13,33 @@ npx tadforce doctor --json
|
|
|
13
13
|
| `next` says | You do |
|
|
14
14
|
|---|---|
|
|
15
15
|
| install the missing tools | `sf-cli` missing: you may run `npm install --global @salesforce/cli` after asking. Node or a browser missing: human action (or `npx @puppeteer/browsers install chrome-headless-shell@stable` + `TADFORCE_CHROME`) |
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
16
|
+
| question `project` | Confirm with the human that this Salesforce project is the one to document → `--project yes`. Not in a project: `doctor` creates nothing; it proposes the projects found nearby → `--project <path>` (then always pass `--cwd <path>`), or `--project create` only if the human asks for a new project here |
|
|
17
|
+
| question `org` | Show the org (alias, user, URL) and ask whether the TAD is about it → `--org <alias>`. Another org, or none connected: the human logs in (`sf org login web --alias <a> [--instance-url https://test.salesforce.com]`, it opens the browser), then ask again and pass `--org <a>`. No org at all: `--source local` |
|
|
18
|
+
| framing questions | Block A of `questions.md`: one message, the recommended answer for each, « accept all » allowed → `--confirm --lang … --client "…" --integrator …|none --author …|none [--specs …|--no-specs] [--scope …]` |
|
|
19
|
+
| question `source` | org (retrieve everything, read-only, several minutes), local, or self (the human retrieves first). The answer **is the go**: ask before passing `--source org` — the retrieve starts right away. `self`: wait until the human says the retrieve is done, then `--source local` |
|
|
20
|
+
| question `branding` | Show the logo and colours found → `--branding keep`, or `--logo <file|url>` |
|
|
21
|
+
| systems / exclusions / features | Blocks B, C and D of `questions.md`, recommended answers shown → write `knowledge/*.yml`, then `--facts-reviewed` (it recomputes the facts with your answers) |
|
|
19
22
|
| `tadforce build` | Ready: write and build |
|
|
20
23
|
|
|
24
|
+
Run by a person in a terminal, a green `doctor` starts the AI agent found on the machine on `/tadforce` (`--agent claude|codex|gemini|cursor`, `--no-launch` to stop there). Run by you (`--json`, or inside an agent session), it never starts another agent: carry on with the writing.
|
|
25
|
+
|
|
21
26
|
What `doctor` does by itself, read-only:
|
|
22
27
|
|
|
23
28
|
- creates the SFDX project and the `tadforce/` workspace if missing, installs the skills for the agents found;
|
|
24
29
|
- reuses the project's `target-org` (`sf config get target-org`) — no separate login step when the project already has one;
|
|
25
|
-
- metadata, as chosen (`source`): **org** (recommended — the org is the truth) retrieves every useful type with a `*` manifest into `tadforce/.snapshot` (`force-app` is never touched; large
|
|
30
|
+
- metadata, as chosen (`source`): **org** (recommended — the org is the truth) retrieves every useful type with a `*` manifest into `tadforce/.snapshot` (`force-app` is never touched; a large org takes 10 to 20 minutes — tell the human before, and follow the progress printed on stderr); **local** reads the package directories; **both** combines them. Then `facts/facts.json`, `facts/org.json` (aggregates), `facts/questions.json`, the chapter skeletons in `tadforce/src/`;
|
|
26
31
|
- branding from evidence: the client logo searched in the snapshot (static resources, content assets, site assets named *logo*), the colours from the org's Lightning theme (`BrandingSet`), else from the logo. Never fetch a logo from the web without the human's go-ahead; when they agree, pass its URL with `--logo`.
|
|
27
32
|
|
|
28
33
|
### Other languages
|
|
29
34
|
|
|
30
35
|
English and French are built in. Any other language (`--lang es`, `de`, `it`, `pt`, `nl`, `ar`, `ja`…) works too: the skeletons are scaffolded in English and `tadforce/i18n/<lang>.json` is created from the English labels. Once, before writing:
|
|
31
36
|
|
|
32
|
-
1. Translate the headings of every `tadforce/src/*/*.md` file
|
|
37
|
+
1. Translate the headings **and the body text** of every `tadforce/src/*/*.md` file (chapter 2 and the axes method hold ready-made paragraphs). Keep the `{#anchors}`, the `{-}` markers, the `<!-- tad:… -->` lines and the `@@…@@` directives as they are — except the caption after `|` in a directive (`@@ERD:A,B|caption@@`), which you translate.
|
|
33
38
|
2. Translate every value of `tadforce/i18n/<lang>.json` (cover, tables, figures, buttons of the HTML). Keep the keys and the `{placeholders}`; set `meta.htmlLang`. Delete the `_translate` line.
|
|
34
|
-
3.
|
|
39
|
+
3. Translate `document.title`, `status`, `classification` and `environment` in `tadforce/tadforce.config.json` (their defaults are English).
|
|
40
|
+
4. Write the sections in that language; Salesforce terms and metadata values stay as they are. Dates and numbers follow the language by themselves.
|
|
35
41
|
|
|
36
|
-
`doctor` shows a reminder
|
|
42
|
+
Batch it: one script or one file write per kind (headings map, labels file), not one edit per heading. `doctor` shows a reminder while the labels file or chapters with English headings remain. To ship the language for everyone, contribute it (issue template « New language »).
|
|
37
43
|
|
|
38
44
|
### Optional inputs
|
|
39
45
|
|
|
@@ -42,6 +42,8 @@ When the comment contradicts the metadata, do not silently obey: answer with the
|
|
|
42
42
|
- add ids to `dropImages` when a capture is no longer useful (keeps the file small).
|
|
43
43
|
4. `npx tadforce build --pdf --docx` — handled comments appear as **resolved**, with your answer, in the panel of the same HTML file. When the readers also edited text in the page, the build keeps a copy (`…_comments_<date>.html`): report those edits into the sources.
|
|
44
44
|
|
|
45
|
+
A comment a reader reopened after its resolution stays open at the next build (warning `comment-reopened`): read the new remark and answer it again with a new resolution.
|
|
46
|
+
|
|
45
47
|
## 4. Report
|
|
46
48
|
|
|
47
49
|
One line per comment: id, section, what changed (or why not). Then the validation score. Ask the human to reopen the new HTML and check the resolved comments.
|
|
@@ -13,14 +13,14 @@ The document already exists. Do not rewrite it: find what changed, and rewrite o
|
|
|
13
13
|
npx tadforce update --json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
It
|
|
16
|
+
It retrieves a fresh snapshot (`source.mode`), recomputes the facts, compares content fingerprints and returns:
|
|
17
17
|
|
|
18
18
|
- `changes.added | changed | removed` — components, by metadata folder (`classes:InvoiceService`, `flows:Case_Routing`…);
|
|
19
19
|
- `counts` — every count that moved (`apex.classes: [132, 140]`);
|
|
20
|
-
- `sections[]` — the sections to review: `id`, `file`, `anchor` (the heading `{#id}` the section sits under) and the
|
|
21
|
-
- `from` → `to` — the version bump, already written to the document history.
|
|
20
|
+
- `sections[]` — the written sections to review: `id`, `file`, `anchor` (the heading `{#id}` the section sits under), `reasons` and `relevance`: **components** (a component it describes was added, changed or removed: rewrite) or **counts** (only numbers moved: check that the text states no number the facts now contradict). Sections still to write are not listed;
|
|
21
|
+
- `from` → `to` — the version bump, already written to the document history (the facts of `from` are archived in `facts/history/v<from>/`, never overwritten).
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
`upToDate: true`: nothing changed — say so and stop.
|
|
24
24
|
|
|
25
25
|
## 2. Ask, once
|
|
26
26
|
|
|
@@ -39,6 +39,8 @@ A section listed only for moved counts usually needs no change: check that the t
|
|
|
39
39
|
|
|
40
40
|
Then, in chapter 15 (Outlook) or the Improvement Axes, record what the update revealed that the client should know (new debt goes to the Axes, never to the TAD).
|
|
41
41
|
|
|
42
|
+
Comments answered before the update (`review.tad.resolutions`) may no longer be true: re-read each one against the new behaviour and correct its answer.
|
|
43
|
+
|
|
42
44
|
## 4. Build and report
|
|
43
45
|
|
|
44
46
|
```bash
|
package/src/build/directives.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Deterministic content generated from facts: appendices, ERDs, figures, KPI tiles, placeholders.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { t, formatNumber } from '../util/i18n.js';
|
|
3
|
+
import { t, formatNumber, formatDate } from '../util/i18n.js';
|
|
4
4
|
import { exists, readText } from '../util/fsx.js';
|
|
5
5
|
import { contextView, layersBlock } from './figures.js';
|
|
6
6
|
|
|
@@ -57,7 +57,11 @@ export function resolveValues(text, ctx, problems) {
|
|
|
57
57
|
const src = kind === 'cfg' ? ctx.cfg : ctx.facts;
|
|
58
58
|
let v = kind === 'n' ? (get(ctx.facts.counts, p) ?? get(ctx.facts, p)) : get(src, p);
|
|
59
59
|
if (kind === 'n' && Array.isArray(v)) v = v.length;
|
|
60
|
-
if (v === undefined || v === null
|
|
60
|
+
if (v === undefined || v === null || v === '') {
|
|
61
|
+
problems.push({ level: 'error', code: 'unknown-placeholder', message: kind === 'cfg' && get(ctx.cfg, p) !== undefined ? `${m} is empty in tadforce.config.json: fill it, or put the sentence inside <!-- tad:if cfg.${p} -->` : `Unknown placeholder ${m}` });
|
|
62
|
+
return `⟨${m.slice(2, -2)}⟩`;
|
|
63
|
+
}
|
|
64
|
+
if (kind === 'cfg' && /date$/i.test(p)) return formatDate(v, ctx.lang);
|
|
61
65
|
return kind === 'n' && typeof v === 'number' ? formatNumber(v, ctx.lang) : String(v);
|
|
62
66
|
});
|
|
63
67
|
}
|
|
@@ -78,14 +82,14 @@ function annexes(ctx) {
|
|
|
78
82
|
...(cfg.integrator.name ? [[t(dict, 'control.integrator'), cfg.integrator.name]] : []),
|
|
79
83
|
...(cfg.document.author ? [[t(dict, 'control.preparedBy'), cfg.document.author]] : []),
|
|
80
84
|
[t(dict, 'control.version'), `${cfg.document.version}${cfg.document.status ? ' — ' + cfg.document.status : ''}`],
|
|
81
|
-
[t(dict, 'control.date'), cfg.document.date],
|
|
82
|
-
[t(dict, 'control.classification'), cfg.document.classification],
|
|
83
|
-
[t(dict, 'control.scope'), t(dict, 'control.scopeValue', { environment: cfg.document.environment, client: cfg.client.name, date: cfg.document.date })
|
|
84
|
-
+ (cfg.scope && (cfg.scope.include || []).length ? ' — ' + t(dict, 'control.scopeLimited', { scope: cfg.scope.
|
|
85
|
+
[t(dict, 'control.date'), formatDate(cfg.document.date, ctx.lang)],
|
|
86
|
+
[t(dict, 'control.classification'), ctx.doc === 'axes' ? t(dict, 'doc.axesKicker') : cfg.document.classification],
|
|
87
|
+
[t(dict, 'control.scope'), t(dict, cfg.source && cfg.source.mode === 'local' ? 'control.scopeValueLocal' : 'control.scopeValue', { environment: cfg.document.environment, client: cfg.client.name, date: formatDate(cfg.document.date, ctx.lang) })
|
|
88
|
+
+ (cfg.scope && (cfg.scope.include || []).length ? ' — ' + t(dict, 'control.scopeLimited', { scope: cfg.scope.label || cfg.scope.include.join(', ') }) : '')],
|
|
85
89
|
]),
|
|
86
90
|
'version-history': () => mdTable([t(dict, 'control.version'), t(dict, 'control.date'), t(dict, 'control.author'), t(dict, 'control.purpose')],
|
|
87
|
-
(cfg.document.history && cfg.document.history.length ? cfg.document.history : [{ version: cfg.document.version, date: cfg.document.date, author: [cfg.document.author, cfg.integrator.name].filter(Boolean).join(' — ') ||
|
|
88
|
-
.map((h) => [h.version, h.date, h.author, h.purpose])),
|
|
91
|
+
(cfg.document.history && cfg.document.history.length ? cfg.document.history : [{ version: cfg.document.version, date: cfg.document.date, author: [cfg.document.author, cfg.integrator.name].filter(Boolean).join(' — ') || '—', purpose: t(dict, 'control.firstVersion', { client: cfg.client.name }) }])
|
|
92
|
+
.map((h) => [h.version, formatDate(h.date, ctx.lang), h.author || '—', h.purpose])),
|
|
89
93
|
legend: () => `<div class="legend">\n<span class="lg actor">${t(dict, 'legend.actor')}</span>\n<span class="lg sf">${t(dict, 'legend.sf')}</span>\n<span class="lg ext">${t(dict, 'legend.ext')}</span>\n<span class="lg data">${t(dict, 'legend.data')}</span>\n</div>`,
|
|
90
94
|
'key-figures': () => mdTable([A('domain'), A('count')], [
|
|
91
95
|
[t(dict, 'keyfig.apps'), n(f.counts.ui.apps)], [t(dict, 'keyfig.pages'), n(f.counts.ui.flexipages)], [t(dict, 'keyfig.lwc'), n(f.counts.ui.lwc)],
|
|
@@ -193,7 +197,8 @@ export function erd(facts, objectNames, caption) {
|
|
|
193
197
|
if (names.length === 1 && names[0] === 'auto') {
|
|
194
198
|
const degree = {};
|
|
195
199
|
for (const o of facts.data.objects.filter((x) => !x.namespace)) {
|
|
196
|
-
|
|
200
|
+
// only objects documented here: an out-of-scope or excluded target never becomes a box
|
|
201
|
+
for (const r of o.relations) for (const to of r.to) { if (to === o.name || to === 'User' || to === 'Group' || !byName.has(to)) continue; degree[o.name] = (degree[o.name] || 0) + 1; degree[to] = (degree[to] || 0) + 1; }
|
|
197
202
|
}
|
|
198
203
|
names = Object.entries(degree).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 12).map(([k]) => k);
|
|
199
204
|
}
|
|
@@ -282,8 +287,8 @@ export function resolveDirectives(text, ctx, problems) {
|
|
|
282
287
|
if (kind === 'FIGURE') {
|
|
283
288
|
if (arg === 'context') return '<div class="fig-body hand">' + contextView(ctx.facts, ctx.glossary, ctx.cfg, ctx.dict) + '</div>\n<p class="figcap">Figure — ' + (caption || t(ctx.dict, 'context.caption')) + '</p>';
|
|
284
289
|
if (arg === 'layers') return layersBlock(ctx.facts, ctx.dict);
|
|
285
|
-
if (arg === 'roles') return rolesFigure(ctx.facts, caption ||
|
|
286
|
-
if (arg === 'integrations') return integrationsFigure(ctx.facts, ctx.cfg, ctx.dict, caption ||
|
|
290
|
+
if (arg === 'roles') return rolesFigure(ctx.facts, caption || t(ctx.dict, 'figures.roles'));
|
|
291
|
+
if (arg === 'integrations') return integrationsFigure(ctx.facts, ctx.cfg, ctx.dict, caption || t(ctx.dict, 'figures.integrations'));
|
|
287
292
|
problems.push({ level: 'error', code: 'unknown-figure', message: `Unknown figure @@FIGURE:${arg}@@` });
|
|
288
293
|
return '';
|
|
289
294
|
}
|
package/src/build/figures.js
CHANGED
|
@@ -115,5 +115,5 @@ export function layersBlock(facts, dict) {
|
|
|
115
115
|
const svc = [c.apex.triggers && L('triggers', c.apex.triggers), c.apex.auraEnabled && L('controllers', c.apex.auraEnabled), L('services', c.apex.classes), (c.apex.batch + c.apex.queueable + c.apex.schedulable + c.apex.future) && L('async')];
|
|
116
116
|
const data = [L('standard'), c.objects.custom && L('custom', c.objects.custom), c.cmdt.types && L('cmdt', c.cmdt.types), L('files')];
|
|
117
117
|
const integ = [c.integration.namedCredentials && L('nc', c.integration.namedCredentials), c.integration.externalCredentials && L('ec', c.integration.externalCredentials), c.integration.restResources && L('rest', c.integration.restResources), c.integration.connectedApps && L('apps2', c.integration.connectedApps)];
|
|
118
|
-
return `<div class="layers">\n${row('', L('presentation'), pres)}\n${row('', L('orchestration'), orch.length && orch.some(Boolean) ? orch : ['—'])}\n${row('', L('
|
|
118
|
+
return `<div class="layers">\n${row('', L('presentation'), pres)}\n${row('', L('orchestration'), orch.length && orch.some(Boolean) ? orch : ['—'])}\n${row('', L('servicesTitle'), svc)}\n${row('data', L('data'), data)}\n${row('int', L('integration'), integ.some(Boolean) ? integ : ['—'])}\n</div>\n<p class="figcap">Figure — ${esc(L('caption'))}</p>`;
|
|
119
119
|
}
|
package/src/build/render.js
CHANGED
|
@@ -3,8 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import MarkdownIt from 'markdown-it';
|
|
4
4
|
import { exists, readText, writeText, readJson } from '../util/fsx.js';
|
|
5
5
|
import { TEMPLATES, PKG_ROOT } from '../util/paths.js';
|
|
6
|
-
import { loadI18n, t } from '../util/i18n.js';
|
|
6
|
+
import { loadI18n, t, formatDate } from '../util/i18n.js';
|
|
7
7
|
import { loadFacts } from '../facts/index.js';
|
|
8
|
+
import { sourceFlags } from '../util/config.js';
|
|
8
9
|
import { CLASSDEFS, renderMermaid, escapeHtml } from './mermaid.js';
|
|
9
10
|
import { resolveConditionals, resolveIncludes, resolveDirectives, resolveValues } from './directives.js';
|
|
10
11
|
|
|
@@ -115,7 +116,7 @@ export async function buildDocument(wsCtx, doc, opts = {}) {
|
|
|
115
116
|
if (!files.length) throw new Error(`No Markdown chapters in ${srcDir}.`);
|
|
116
117
|
|
|
117
118
|
let text = files.map((f) => readText(path.join(srcDir, f))).join('\n\n');
|
|
118
|
-
const scope = { modules: facts.modules, counts: facts.counts, org: facts.org, cfg, facts, glossary };
|
|
119
|
+
const scope = { modules: facts.modules, counts: facts.counts, org: facts.org, cfg, facts, glossary, source: sourceFlags(cfg) };
|
|
119
120
|
const docName = doc === 'tad' ? t(dict, 'meta.docName') : t(dict, 'meta.axesName');
|
|
120
121
|
const ctx = { facts, cfg, dict, lang, glossary, doc, docName, chapterTitle: (k) => `{{title:ch-${k}}}` };
|
|
121
122
|
text = resolveConditionals(text, scope);
|
|
@@ -221,7 +222,7 @@ export async function buildDocument(wsCtx, doc, opts = {}) {
|
|
|
221
222
|
'{{DOC_SUBTITLE}}': escapeHtml(cfg.document.subtitle || [cfg.client.name, ...facts.modules.clouds.filter((x) => !['events', 'shield', 'slack'].includes(x.id)).slice(0, 3).map((x) => x.name)].join(' · ')),
|
|
222
223
|
'{{DOC_REFERENCE}}': escapeHtml(doc === 'tad' ? cfg.document.reference : cfg.document.reference + '-AX'),
|
|
223
224
|
'{{DOC_VERSION}}': escapeHtml(`${cfg.document.version}${cfg.document.status ? ' — ' + cfg.document.status : ''}`),
|
|
224
|
-
'{{DOC_DATE}}': escapeHtml(cfg.document.date),
|
|
225
|
+
'{{DOC_DATE}}': escapeHtml(formatDate(cfg.document.date, lang)),
|
|
225
226
|
'{{DOC_CLASSIFICATION}}': escapeHtml(doc === 'tad' ? cfg.document.classification : t(dict, 'doc.axesKicker')),
|
|
226
227
|
'{{INTEGRATOR_NAME}}': escapeHtml(integrator || cfg.client.name),
|
|
227
228
|
'{{COVER_LOGOS}}': coverLogos,
|
|
@@ -233,7 +234,7 @@ export async function buildDocument(wsCtx, doc, opts = {}) {
|
|
|
233
234
|
'{{INTEGRATOR_LOGO}}': sideLogo,
|
|
234
235
|
'{{CLIENT_LOGO}}': logo('client'),
|
|
235
236
|
'{{SIDE_TITLE}}': escapeHtml(`${docName} — ${cfg.document.title}`),
|
|
236
|
-
'{{SIDE_SUB}}': escapeHtml(`${cfg.client.name} · v${cfg.document.version} · ${cfg.document.date}`),
|
|
237
|
+
'{{SIDE_SUB}}': escapeHtml(`${cfg.client.name} · v${cfg.document.version} · ${formatDate(cfg.document.date, lang)}`),
|
|
237
238
|
'{{FOOTER}}': escapeHtml(`${integrator ? integrator + ' · ' : ''}${internal ? '' : cfg.client.name + ' · '}${doc === 'tad' ? cfg.document.classification : t(dict, 'doc.axesKicker')}`) + ` · <span class="gen">${escapeHtml(t(dict, 'doc.generatedBy'))}</span>`,
|
|
238
239
|
'{{I18N_JSON}}': JSON.stringify({ editor: dict.editor }).replace(/</g, '\\u003c'),
|
|
239
240
|
'{{BRAND_JSON}}': JSON.stringify(brand).replace(/</g, '\\u003c'),
|
|
@@ -271,7 +272,9 @@ export async function buildDocument(wsCtx, doc, opts = {}) {
|
|
|
271
272
|
for (const c of carried) {
|
|
272
273
|
if (c.status === 'résolu') c.status = 'resolved';
|
|
273
274
|
const res = review.resolutions && review.resolutions[c.id];
|
|
274
|
-
|
|
275
|
+
// already answered with this resolution, yet open again: a reader reopened it — keep it open
|
|
276
|
+
if (res && c.status === 'open' && c.response === res) problems.push({ level: 'warn', code: 'comment-reopened', message: `Comment ${c.id} was reopened by a reader after its resolution: answer it again (review.${doc}.resolutions).` });
|
|
277
|
+
else if (res) { c.status = 'resolved'; c.response = res; }
|
|
275
278
|
if ((review.dropImages || []).includes(c.id)) c.image = null;
|
|
276
279
|
}
|
|
277
280
|
if (carried.length) {
|
package/src/commands/docs.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { exists, writeJson, readText, copyDir, stableStringify } from '../util/fsx.js';
|
|
5
|
-
import { loadWorkspace, sourceDirs } from '../util/config.js';
|
|
5
|
+
import { loadWorkspace, sourceDirs, sourceFlags } from '../util/config.js';
|
|
6
|
+
import { resolveConditionals } from '../build/directives.js';
|
|
6
7
|
import { TEMPLATES } from '../util/paths.js';
|
|
7
8
|
import { LANGUAGES, normalizeLang, templateLang } from '../util/i18n.js';
|
|
8
9
|
import { extractFacts, readCodeAnalyzer, loadFacts } from '../facts/index.js';
|
|
@@ -12,14 +13,17 @@ import { buildDocument, readComments, outputName, DOCS } from '../build/render.j
|
|
|
12
13
|
import { exportPdf, exportDocx } from '../build/export.js';
|
|
13
14
|
import { closeBrowser } from '../build/browser.js';
|
|
14
15
|
import { validateDocument } from '../validate/index.js';
|
|
15
|
-
import { log, c } from '../util/log.js';
|
|
16
|
+
import { log, c, setQuiet } from '../util/log.js';
|
|
16
17
|
import * as org from './org.js';
|
|
17
18
|
|
|
18
19
|
const docsOf = (args) => (!args.doc || args.doc === 'all' ? DOCS : String(args.doc).split(','));
|
|
19
20
|
|
|
20
21
|
export async function facts(args) {
|
|
21
22
|
const ctx = loadWorkspace(args.cwd || process.cwd());
|
|
22
|
-
|
|
23
|
+
if (args.json) setQuiet(true); // stdout carries the JSON only
|
|
24
|
+
// --source names a folder: one that does not exist is an error, never an empty inventory
|
|
25
|
+
if (typeof args.source === 'string' && !exists(path.resolve(args.source))) throw new Error(`--source: no such folder ${path.resolve(args.source)} (use the folder that holds the metadata, e.g. force-app).`);
|
|
26
|
+
const dirs = typeof args.source === 'string' ? [path.resolve(args.source)] : sourceDirs(ctx);
|
|
23
27
|
if (!dirs.length) throw new Error('No metadata found: run `tadforce retrieve` (or point --source at a force-app folder).');
|
|
24
28
|
log.banner('facts');
|
|
25
29
|
log.step(`Reading metadata from ${dirs.map((d) => path.relative(ctx.project, d) || '.').join(', ')}`);
|
|
@@ -41,6 +45,8 @@ export async function facts(args) {
|
|
|
41
45
|
const questions = buildQuestions(f, orgFacts);
|
|
42
46
|
writeJson(ctx.p('facts', 'questions.json'), questions);
|
|
43
47
|
const k = f.counts;
|
|
48
|
+
if (args.json) { console.log(JSON.stringify({ components: f.inventory.length, counts: k, modules: f.modules.products, sources: f.sources, questions: { systems: questions.systems.length, exclusions: questions.exclusions.total } }, null, 2)); return f.inventory.length ? 0 : 1; }
|
|
49
|
+
if (!f.inventory.length) log.warn(`No component found in ${dirs.map((d) => path.relative(ctx.project, d) || '.').join(', ')}: is the metadata there?`);
|
|
44
50
|
log.ok(`facts.json: ${k.apex.classes} classes (+${k.apex.tests} tests), ${k.apex.triggers} triggers, ${k.flows.active} active flows, ${k.ui.lwc} LWC, ${k.objects.custom} custom objects, ${k.security.permissionSetGroups} PSG`);
|
|
45
51
|
log.info(`modules: ${Object.entries(f.modules).filter(([kk, v]) => v === true).map(([kk]) => kk).join(', ') || 'none'}${f.modules.products.length ? ' · products: ' + f.modules.products.join(', ') : ''}`);
|
|
46
52
|
if (questions.systems.length) log.human(`${questions.systems.length} external endpoint(s) need a system name → knowledge/glossary.yml (see facts/questions.json).`);
|
|
@@ -52,7 +58,7 @@ export async function facts(args) {
|
|
|
52
58
|
export function buildQuestions(f, orgFacts) {
|
|
53
59
|
const byReason = {};
|
|
54
60
|
for (const cnd of f.exclusions.candidates) {
|
|
55
|
-
const key = cnd.reasons[0].replace(/name suggests .*/, 'name suggests test
|
|
61
|
+
const key = cnd.reasons[0].replace(/name suggests .*/, 'name suggests an unused component (test, demo, POC, sample, old, legacy…)');
|
|
56
62
|
(byReason[key] ||= []).push(`${cnd.type}:${cnd.name}`);
|
|
57
63
|
}
|
|
58
64
|
if (orgFacts && orgFacts.volumes) {
|
|
@@ -116,8 +122,10 @@ export async function build(args) {
|
|
|
116
122
|
built.push(r);
|
|
117
123
|
if (r.pending.length) log.warn(`${r.pending.length} section(s) of ${doc} still to write — your agent writes them (tadforce next), then build again.`);
|
|
118
124
|
log.ok(`${path.relative(ctx.project, r.html)} (${r.sizeKB} KB, ${r.figures} figures)`);
|
|
119
|
-
|
|
120
|
-
const
|
|
125
|
+
// naming one format (--pdf) means that one only; with no flag, the config decides
|
|
126
|
+
const named = args.pdf !== undefined || args.docx !== undefined;
|
|
127
|
+
const wantPdf = named ? args.pdf === true : ctx.cfg.outputs.pdf;
|
|
128
|
+
const wantDocx = named ? args.docx === true : ctx.cfg.outputs.docx;
|
|
121
129
|
const archive = path.join(path.dirname(r.html), outputName(ctx.cfg, doc, { versioned: true }));
|
|
122
130
|
if (wantPdf && args.render !== false) { const p = archive + '.pdf'; const s = await exportPdf(r.html, p); log.ok(`${path.relative(ctx.project, p)} (${Math.round(s / 1024)} KB)`); }
|
|
123
131
|
if (wantDocx && args.render !== false) { const p = archive + '.docx'; const s = await exportDocx(r.html, p); log.ok(`${path.relative(ctx.project, p)} (${Math.round(s / 1024)} KB)`); }
|
|
@@ -128,6 +136,9 @@ export async function build(args) {
|
|
|
128
136
|
} finally {
|
|
129
137
|
await closeBrowser();
|
|
130
138
|
}
|
|
139
|
+
// the sections listed by `update` are now in a built document: a later update starts afresh
|
|
140
|
+
const upd = ctx.p('facts', 'update.json');
|
|
141
|
+
if (built.length && exists(upd)) { const u = JSON.parse(readText(upd)); if (!u.built) writeJson(upd, { ...u, built: true }); }
|
|
131
142
|
if (args.open && built[0]) { const { open } = await import('./review.js'); await open(built[0].html); }
|
|
132
143
|
return args.strict && failed ? 1 : 0;
|
|
133
144
|
}
|
|
@@ -135,7 +146,10 @@ export async function build(args) {
|
|
|
135
146
|
function report(v, doc) {
|
|
136
147
|
const icon = v.ok ? c.green('✔') : c.red('✖');
|
|
137
148
|
console.log(`${icon} validate ${doc}: score ${v.score}/100 — ${v.errors.length} error(s), ${v.warnings.length} warning(s)`);
|
|
138
|
-
|
|
149
|
+
// what a reader must act on first, then real problems, then the sections still to write (often many)
|
|
150
|
+
const urgent = (w) => ['comment-reopened', 'open-comments', 'backup', 'comments-missing'].includes(w.code);
|
|
151
|
+
const pending = v.errors.filter((e) => e.code === 'pending');
|
|
152
|
+
const show = [...v.warnings.filter(urgent), ...v.errors.filter((e) => e.code !== 'pending'), ...v.warnings.filter((w) => !urgent(w)), ...pending];
|
|
139
153
|
for (const i of show.slice(0, 40)) console.log(` ${i.level === 'error' ? c.red('error') : c.yellow('warn ')} ${c.dim(i.code)} ${i.message}${i.where ? c.dim(' ' + i.where) : ''}`);
|
|
140
154
|
const hidden = v.errors.length + v.warnings.length - Math.min(40, show.length);
|
|
141
155
|
if (hidden > 0) console.log(c.dim(` … ${hidden} more (tadforce validate --json)`));
|
|
@@ -177,19 +191,26 @@ export async function comments(args) {
|
|
|
177
191
|
export async function status(args) {
|
|
178
192
|
const ctx = loadWorkspace(args.cwd || process.cwd());
|
|
179
193
|
const f = loadFacts(ctx.ws);
|
|
180
|
-
const
|
|
181
|
-
log.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
const report = { source: ctx.cfg.source.mode || 'org', org: ctx.cfg.org.alias || null, components: f ? f.inventory.length : 0, pending: {} };
|
|
195
|
+
const line = (ok, s) => { if (!args.json) console.log(`${ok ? c.green('✔') : c.dim('○')} ${s}`); };
|
|
196
|
+
if (!args.json) log.banner('status');
|
|
197
|
+
if (ctx.cfg.source.mode === 'local') line(true, 'source: local metadata of the project');
|
|
198
|
+
else {
|
|
199
|
+
line(!!ctx.cfg.org.alias, `org: ${ctx.cfg.org.alias || 'not chosen yet (tadforce doctor)'}`);
|
|
200
|
+
line(exists(ctx.p(ctx.cfg.source.dir)), 'metadata retrieved from the org');
|
|
201
|
+
}
|
|
185
202
|
line(!!f, `facts${f ? ` (${f.inventory.length} components${f.org ? ', org facts' : ''})` : ''}`);
|
|
186
203
|
for (const doc of DOCS) {
|
|
187
204
|
const dir = ctx.p('src', doc);
|
|
188
205
|
if (!exists(dir)) { line(false, `src/${doc}`); continue; }
|
|
189
|
-
|
|
206
|
+
// same count as next and validate: sections inside a false <!-- tad:if --> are not part of this document
|
|
207
|
+
const scope = f ? { modules: f.modules, counts: f.counts, org: f.org, cfg: ctx.cfg, facts: f, glossary: ctx.glossary(), source: sourceFlags(ctx.cfg) } : {};
|
|
208
|
+
const text = fs.readdirSync(dir).filter((x) => x.endsWith('.md')).map((x) => resolveConditionals(readText(path.join(dir, x)), scope)).join('\n');
|
|
190
209
|
const pending = (text.match(/<!--\s*tad:pending/g) || []).length;
|
|
210
|
+
report.pending[doc] = pending;
|
|
191
211
|
line(pending === 0, `src/${doc}: ${pending ? pending + ' section(s) to write' : 'written'}`);
|
|
192
212
|
}
|
|
213
|
+
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
193
214
|
return 0;
|
|
194
215
|
}
|
|
195
216
|
|