in2-agent-server 0.20260821.185429
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/README.md +369 -0
- package/agents/plan-requirement.md +32 -0
- package/dist/campfire/configItem.js +201 -0
- package/dist/campfire/configItemDescription.js +379 -0
- package/dist/campfire/deriveFromBundle.js +1013 -0
- package/dist/cli.js +465 -0
- package/dist/core/betaFeedback.js +493 -0
- package/dist/core/chapterInstructions.js +473 -0
- package/dist/core/claudeAssets.js +61 -0
- package/dist/core/gate.js +201 -0
- package/dist/core/ids.js +9 -0
- package/dist/core/install.js +60 -0
- package/dist/core/operatorQa.js +462 -0
- package/dist/core/orgCallMeter.js +178 -0
- package/dist/core/parcels.js +1235 -0
- package/dist/core/paths.js +105 -0
- package/dist/core/phases.js +225 -0
- package/dist/core/planFile.js +251 -0
- package/dist/core/planFragments.js +119 -0
- package/dist/core/readsLog.js +398 -0
- package/dist/core/registration.js +24 -0
- package/dist/core/result.js +38 -0
- package/dist/core/resume.js +233 -0
- package/dist/core/revisionChain.js +351 -0
- package/dist/core/richText.js +61 -0
- package/dist/core/semanticHash.js +89 -0
- package/dist/core/sessionOrg.js +60 -0
- package/dist/core/sfQuery.js +19 -0
- package/dist/core/spawn.js +182 -0
- package/dist/core/supervisorContract.js +230 -0
- package/dist/core/telemetry.js +1297 -0
- package/dist/core/telemetryContext.js +154 -0
- package/dist/core/telemetrySpool.js +244 -0
- package/dist/core/transcriptUpload.js +516 -0
- package/dist/core/update.js +363 -0
- package/dist/core/viewer.js +117 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/writeGrants.js +93 -0
- package/dist/core/writebackManifest.js +92 -0
- package/dist/direct/applyLoop.js +2516 -0
- package/dist/direct/batchReport.js +510 -0
- package/dist/direct/comparatorNoise.js +75 -0
- package/dist/direct/containers.js +85 -0
- package/dist/direct/contamination.js +221 -0
- package/dist/direct/crud.js +56 -0
- package/dist/direct/destructive.js +168 -0
- package/dist/direct/knownTokens.js +94 -0
- package/dist/direct/lint.js +382 -0
- package/dist/direct/metadataSchema.generated.json +1 -0
- package/dist/direct/metadataSchema.js +179 -0
- package/dist/direct/operatorPage.js +349 -0
- package/dist/direct/orgRepo.js +1531 -0
- package/dist/direct/orgRepoSpawnCount.js +21 -0
- package/dist/direct/plannedScope.js +119 -0
- package/dist/direct/preImageTargets.js +63 -0
- package/dist/direct/refVerify.js +191 -0
- package/dist/direct/registry.js +129 -0
- package/dist/direct/removals.js +262 -0
- package/dist/direct/rollback.js +1513 -0
- package/dist/direct/rollbackRun.js +1360 -0
- package/dist/direct/sessionProjection.js +126 -0
- package/dist/direct/snapshotComparability.js +165 -0
- package/dist/direct/sourceTree.js +113 -0
- package/dist/direct/unstatedChanges.js +106 -0
- package/dist/direct/waveAssembly.js +556 -0
- package/dist/direct/waveCounts.js +19 -0
- package/dist/direct/waveDerivation.js +152 -0
- package/dist/direct/waveGrade.js +185 -0
- package/dist/index.js +90 -0
- package/dist/orgread/customFieldSnapshot.js +48 -0
- package/dist/orgread/fieldMetadata.js +178 -0
- package/dist/orgread/genericSnapshot.js +61 -0
- package/dist/orgread/layoutSnapshot.js +151 -0
- package/dist/orgread/salesforceOrgReader.js +184 -0
- package/dist/orgread/snapshotTypes.js +9 -0
- package/dist/projection/bridge.js +26 -0
- package/dist/projection/diff.js +141 -0
- package/dist/projection/mapmd.js +213 -0
- package/dist/projection/nameAmbiguity.js +106 -0
- package/dist/projection/precedent.js +136 -0
- package/dist/projection/project.js +214 -0
- package/dist/projection/scope.js +110 -0
- package/dist/projection/types.js +5 -0
- package/dist/tools/analyze.js +322 -0
- package/dist/tools/auth.js +915 -0
- package/dist/tools/campfire.js +1742 -0
- package/dist/tools/closeBatch.js +154 -0
- package/dist/tools/closeText.js +35 -0
- package/dist/tools/direct.js +1613 -0
- package/dist/tools/feedback.js +318 -0
- package/dist/tools/operator.js +287 -0
- package/dist/tools/org.js +739 -0
- package/dist/tools/phase.js +249 -0
- package/dist/tools/projection.js +172 -0
- package/dist/tools/session.js +313 -0
- package/dist/tools/update.js +82 -0
- package/dist/tools/waves.js +793 -0
- package/package.json +48 -0
- package/reference/campfire-configuration-items-guide.md +710 -0
- package/reference/deployability-cribs.md +119 -0
- package/reference/operator-voice.md +141 -0
- package/reference/org-read-cribs.md +34 -0
- package/reference/scoped-dml-interim.md +68 -0
- package/reference/supervisor-playbook.md +313 -0
- package/reference/templates.md +233 -0
- package/skills/in2-delivery/SKILL.md +322 -0
- package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
- package/skills/in2-delivery/references/deployability-cribs.md +119 -0
- package/skills/in2-delivery/references/operator-voice.md +141 -0
- package/skills/in2-delivery/references/org-read-cribs.md +34 -0
- package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
- package/skills/in2-delivery/references/templates.md +233 -0
- package/skills/plan-requirement/SKILL.md +188 -0
- package/viewer/index.html +899 -0
- package/viewer/server.mjs +433 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
function asStr(v) {
|
|
2
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
3
|
+
}
|
|
4
|
+
function truncate(v, n) {
|
|
5
|
+
return v.length > n ? `${v.slice(0, n)}…` : v;
|
|
6
|
+
}
|
|
7
|
+
function fieldFacets(md) {
|
|
8
|
+
const type = asStr(md.type) ?? "?";
|
|
9
|
+
const parts = [type];
|
|
10
|
+
const label = asStr(md.label);
|
|
11
|
+
if (label)
|
|
12
|
+
parts[0] = `${type} "${label}"`;
|
|
13
|
+
if (md.required === true)
|
|
14
|
+
parts.push("required");
|
|
15
|
+
if (typeof md.length === "number")
|
|
16
|
+
parts.push(`length ${md.length}`);
|
|
17
|
+
if (typeof md.precision === "number" || typeof md.scale === "number") {
|
|
18
|
+
parts.push(`precision ${md.precision ?? "?"}, scale ${md.scale ?? "?"}`);
|
|
19
|
+
}
|
|
20
|
+
const valueSet = md.valueSet;
|
|
21
|
+
if (valueSet && typeof valueSet === "object") {
|
|
22
|
+
const values = Array.isArray(valueSet.values) ? valueSet.values.filter((v) => typeof v === "string") : [];
|
|
23
|
+
if (values.length > 0)
|
|
24
|
+
parts.push(`values: ${values.join(" | ")}`);
|
|
25
|
+
if (asStr(valueSet.valueSetName))
|
|
26
|
+
parts.push(`globalValueSet ${valueSet.valueSetName}`);
|
|
27
|
+
parts.push(valueSet.restricted === true ? "restricted" : "unrestricted");
|
|
28
|
+
const controlling = asStr(valueSet.controllingField);
|
|
29
|
+
if (controlling) {
|
|
30
|
+
const matrix = Array.isArray(valueSet.valueSettings) ? valueSet.valueSettings.length : 0;
|
|
31
|
+
parts.push(`DEPENDENT on ${controlling} (${matrix} matrix entries)`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (md.type === "Picklist" || md.type === "MultiselectPicklist") {
|
|
35
|
+
parts.push(asStr(md.defaultValue) ? `default ${md.defaultValue}` : "no default");
|
|
36
|
+
}
|
|
37
|
+
else if (asStr(md.defaultValue)) {
|
|
38
|
+
parts.push(`default ${md.defaultValue}`);
|
|
39
|
+
}
|
|
40
|
+
const formula = asStr(md.formula);
|
|
41
|
+
if (formula)
|
|
42
|
+
parts.push(`formula: ${truncate(formula.replace(/\s+/g, " "), 80)}`);
|
|
43
|
+
if (asStr(md.inlineHelpText))
|
|
44
|
+
parts.push("help✓");
|
|
45
|
+
if (asStr(md.referenceTo))
|
|
46
|
+
parts.push(`→ ${md.referenceTo}`);
|
|
47
|
+
return parts.join(" — ");
|
|
48
|
+
}
|
|
49
|
+
function objectFacets(md) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (asStr(md.sharingModel))
|
|
52
|
+
parts.push(`sharing ${md.sharingModel}`);
|
|
53
|
+
const flags = [];
|
|
54
|
+
if (md.enableActivities === true)
|
|
55
|
+
flags.push("activities");
|
|
56
|
+
if (md.enableReports === true)
|
|
57
|
+
flags.push("reports");
|
|
58
|
+
if (md.enableSearch === true)
|
|
59
|
+
flags.push("search");
|
|
60
|
+
if (md.enableHistory === true)
|
|
61
|
+
flags.push("history");
|
|
62
|
+
if (flags.length)
|
|
63
|
+
parts.push(`enabled: ${flags.join(", ")}`);
|
|
64
|
+
const nameField = md.nameField;
|
|
65
|
+
if (nameField && typeof nameField === "object") {
|
|
66
|
+
parts.push(`nameField ${asStr(nameField.type) ?? "?"}${asStr(nameField.label) ? ` "${nameField.label}"` : ""}`);
|
|
67
|
+
}
|
|
68
|
+
return parts.join(" | ");
|
|
69
|
+
}
|
|
70
|
+
export function renderMap(manifest, byFile) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push(`# Org Projection — ${manifest.orgAlias} (${manifest.orgId})`);
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push(`Captured ${manifest.createdAt} · API ${manifest.apiVersion} · ${manifest.counts.components} components · ${manifest.counts.errors} errors · scope ${JSON.stringify(manifest.scope)}`);
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("Every line links the canonical JSON snapshot (the same normalized shape the verifier gate reads).");
|
|
77
|
+
// ORG TRAITS FIRST, because they change what a CORRECT file looks like and nothing in the
|
|
78
|
+
// component list reveals them. On a multi-currency org every layout carries CurrencyIsoCode, so
|
|
79
|
+
// an authored Layout that omits it silently drops a field — measured as real loss, and caught on
|
|
80
|
+
// CF78 only at wave 4's pre-flight, long after the analyzer that wrote the file could act on it.
|
|
81
|
+
const t = manifest.orgTraits;
|
|
82
|
+
if (t) {
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("## Org traits — these change what correct authoring looks like");
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push(`- multi-currency: **${t.multiCurrency ? "YES" : "no"}**${t.multiCurrency ? " — every layout carries CurrencyIsoCode; omitting it from an authored Layout DROPS it" : ""}`);
|
|
87
|
+
lines.push(`- person accounts: **${t.personAccounts ? "YES" : "no"}**`);
|
|
88
|
+
lines.push(`- call center / CTI: **${t.callCenter ? "YES" : "no"}**${t.callCenter ? "" : " — an 'if available' softphone requirement is NOT available"}`);
|
|
89
|
+
// Probe caveats and evidence overrides. Without these a reader cannot tell a measured "no"
|
|
90
|
+
// from a probe that never ran — the distinction that made CF79's trait line untrustworthy.
|
|
91
|
+
for (const n of t.notes.filter((n) => n.includes("unavailable") || n.includes("evidence"))) {
|
|
92
|
+
lines.push(`- note: ${n}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// AMBIGUOUS SCOPE NAMES, immediately after the traits and before any component list — because a
|
|
96
|
+
// reader who takes the snapshot below at face value is reading a component that may not be the one
|
|
97
|
+
// the scope meant. Live-caught (EV-31…40): `Profile: Admin` in an org holding two profiles that
|
|
98
|
+
// answer to "Admin", captured silently as though there had been only one.
|
|
99
|
+
const ambiguities = manifest.scopeAmbiguities ?? [];
|
|
100
|
+
if (ambiguities.length > 0) {
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("## Scope names that did NOT resolve to one component");
|
|
103
|
+
lines.push("");
|
|
104
|
+
for (const a of ambiguities) {
|
|
105
|
+
lines.push(`- \`${a.typeName}: ${a.requestedName}\` — ${a.note}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Values the org has already accepted for elements no schema can validate. This is what the
|
|
109
|
+
// UNPROVEN_ORG_TOKEN tier compares against, and printing it here means an analyzer can read the
|
|
110
|
+
// answer instead of querying for it — the hunt that cost 10 minutes on CF77 and 13 on CF78.
|
|
111
|
+
const precedentValues = new Map();
|
|
112
|
+
const harvest = (node, typeName) => {
|
|
113
|
+
if (Array.isArray(node))
|
|
114
|
+
return void node.forEach((n) => harvest(n, typeName));
|
|
115
|
+
if (!node || typeof node !== "object")
|
|
116
|
+
return;
|
|
117
|
+
for (const [k, v] of Object.entries(node)) {
|
|
118
|
+
if ((k === "componentName" || k === "motif") && typeof v === "string" && v) {
|
|
119
|
+
const key = `${typeName}.${k}`;
|
|
120
|
+
if (!precedentValues.has(key))
|
|
121
|
+
precedentValues.set(key, new Set());
|
|
122
|
+
precedentValues.get(key).add(v);
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
harvest(v, typeName);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
for (const c of manifest.components) {
|
|
129
|
+
if (!c.found)
|
|
130
|
+
continue;
|
|
131
|
+
const snap = byFile.get(c.file);
|
|
132
|
+
if (snap?.state?.metadata)
|
|
133
|
+
harvest(snap.state.metadata, c.typeName);
|
|
134
|
+
}
|
|
135
|
+
if (precedentValues.size > 0) {
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push("## Proven in this org — values already accepted for elements no schema can validate");
|
|
138
|
+
lines.push("");
|
|
139
|
+
for (const [key, set] of [...precedentValues].sort()) {
|
|
140
|
+
lines.push(`- \`${key}\`: ${[...set].sort().map((v) => `\`${v}\``).join(", ")}`);
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("A value NOT listed here is unproven — author it only with a stated fallback, and expect `UNPROVEN_ORG_TOKEN`.");
|
|
144
|
+
}
|
|
145
|
+
const byKey = new Map(manifest.components.map((c) => [`${c.typeName}:${c.fullName}`, c]));
|
|
146
|
+
for (const obj of manifest.objects) {
|
|
147
|
+
const entry = byKey.get(`CustomObject:${obj.objectApiName}`);
|
|
148
|
+
const snap = entry ? byFile.get(entry.file) : undefined;
|
|
149
|
+
const md = (snap?.state.metadata ?? null);
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(`## ${obj.objectApiName}${md && asStr(md.label) ? ` — "${md.label}"` : ""}`);
|
|
152
|
+
if (entry)
|
|
153
|
+
lines.push(`Snapshot: ${entry.file}${entry.found ? "" : " — NOT FOUND"}`);
|
|
154
|
+
if (md) {
|
|
155
|
+
const facets = objectFacets(md);
|
|
156
|
+
if (facets)
|
|
157
|
+
lines.push(facets);
|
|
158
|
+
}
|
|
159
|
+
const fields = manifest.components.filter((c) => c.typeName === "CustomField" && c.fullName.startsWith(`${obj.objectApiName}.`));
|
|
160
|
+
if (fields.length) {
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push(`### Custom fields (${fields.length})`);
|
|
163
|
+
for (const f of fields) {
|
|
164
|
+
const fieldName = f.fullName.slice(obj.objectApiName.length + 1);
|
|
165
|
+
const fsnap = byFile.get(f.file);
|
|
166
|
+
const fmd = (fsnap?.state.metadata ?? null);
|
|
167
|
+
const facets = fmd ? fieldFacets(fmd) : "READ FAILED";
|
|
168
|
+
lines.push(`- ${fieldName} — ${facets} → ${f.file}${f.found ? "" : " (NOT FOUND)"}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (obj.standardFields.length) {
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(`### Standard fields (${obj.standardFields.length}, names only)`);
|
|
174
|
+
lines.push(obj.standardFields.map((s) => `${s.name} (${s.dataType})`).join(", "));
|
|
175
|
+
}
|
|
176
|
+
const rules = manifest.components.filter((c) => c.typeName === "ValidationRule" && c.fullName.startsWith(`${obj.objectApiName}.`));
|
|
177
|
+
if (rules.length) {
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(`### Validation rules (${rules.length})`);
|
|
180
|
+
for (const r of rules) {
|
|
181
|
+
lines.push(`- ${r.fullName.slice(obj.objectApiName.length + 1)} → ${r.file}${r.found ? "" : " (NOT FOUND)"}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (obj.layouts.length) {
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(`### Layouts (${obj.layouts.length}, names only — project explicitly via components scope when needed)`);
|
|
187
|
+
for (const l of obj.layouts)
|
|
188
|
+
lines.push(`- ${l}`);
|
|
189
|
+
}
|
|
190
|
+
if (obj.errors.length) {
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push("### Enumeration errors (fail-closed — these lists may be incomplete)");
|
|
193
|
+
for (const e of obj.errors)
|
|
194
|
+
lines.push(`- ${e}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const loose = manifest.components.filter((c) => c.file.startsWith("components/") || c.file.startsWith("components\\"));
|
|
198
|
+
if (loose.length) {
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(`## Other components (${loose.length})`);
|
|
201
|
+
for (const c of loose) {
|
|
202
|
+
lines.push(`- ${c.typeName} ${c.fullName} → ${c.file}${c.found ? "" : " (NOT FOUND)"}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (manifest.enumerationErrors.length) {
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push("## Scope errors (fail-closed — the projection may be incomplete)");
|
|
208
|
+
for (const e of manifest.enumerationErrors)
|
|
209
|
+
lines.push(`- ${e}`);
|
|
210
|
+
}
|
|
211
|
+
lines.push("");
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Scope names that resolve to more than one component — the projection's silent choice, made loud.
|
|
2
|
+
//
|
|
3
|
+
// A projection reads what the caller NAMED. For nearly every type that is unambiguous, because the
|
|
4
|
+
// name the caller writes is the name the Metadata API resolves. Profile is the exception, and it
|
|
5
|
+
// cost a run.
|
|
6
|
+
//
|
|
7
|
+
// A Profile has TWO names. Its Metadata API fullName is its API name; `SELECT Name FROM Profile` —
|
|
8
|
+
// which is what `list_components` hands the supervisor — returns its LABEL, and for standard
|
|
9
|
+
// profiles the two differ ("System Administrator" is read as `Admin`). So a CUSTOM profile whose
|
|
10
|
+
// label is "Admin" and the standard profile whose API name is `Admin` are two different components
|
|
11
|
+
// that a scope of `Profile: Admin` cannot tell apart. `metadata.read()` answers with one of them and
|
|
12
|
+
// says nothing about the other, and the projection recorded that answer as though it were the only
|
|
13
|
+
// candidate.
|
|
14
|
+
//
|
|
15
|
+
// Live-caught (EV-31…40, papercut §6): three admin-ish profiles in the org, two answering to
|
|
16
|
+
// "Admin", and the projection carried no hint that a choice had been made at all.
|
|
17
|
+
//
|
|
18
|
+
// PROFILE ONLY, deliberately. Other types have label/API-name splits too (RecordType, Layout), but
|
|
19
|
+
// no run has yet shown one silently mis-resolving here, and inventing candidate enumeration for a
|
|
20
|
+
// type the org has not complained about is how a check like this grows into a liability. A second
|
|
21
|
+
// type earns its place when a run proves it needs one.
|
|
22
|
+
/**
|
|
23
|
+
* The (API name, label) pair for every profile in the org.
|
|
24
|
+
*
|
|
25
|
+
* There is no SOQL route to a profile's API name through the `Profile` object itself — `Profile.Name`
|
|
26
|
+
* is the label. The profile-owned PermissionSet rows are the documented way round it: for a set with
|
|
27
|
+
* `IsOwnedByProfile = true`, `PermissionSet.Name` is the owning profile's API name while
|
|
28
|
+
* `Profile.Name` is its label. (`list_components` already knows the other half of this quirk — it
|
|
29
|
+
* filters PermissionSet to `IsOwnedByProfile = false` so profile-owned sets do not read as real ones.)
|
|
30
|
+
*
|
|
31
|
+
* DOC-DERIVED shape, not yet confirmed against a live org — which is exactly why the whole check is
|
|
32
|
+
* best-effort. If this query is wrong somewhere, the projection is precisely what it was before.
|
|
33
|
+
*/
|
|
34
|
+
const PROFILE_CANDIDATES_SOQL = "SELECT Name, Profile.Name FROM PermissionSet WHERE IsOwnedByProfile = true";
|
|
35
|
+
const describe = (c) => `\`${c.apiName}\`${c.label ? ` (labelled "${c.label}")` : ""}`;
|
|
36
|
+
async function profileCandidates(reader) {
|
|
37
|
+
// Regular-API SOQL, like the trait probes: PermissionSet is not a Tooling object, and a reader
|
|
38
|
+
// without soqlQuery degrades to "not checked" rather than to a fabricated all-clear.
|
|
39
|
+
const soql = reader.soqlQuery?.bind(reader);
|
|
40
|
+
if (!soql)
|
|
41
|
+
return [];
|
|
42
|
+
const rows = await soql(PROFILE_CANDIDATES_SOQL);
|
|
43
|
+
return rows.records
|
|
44
|
+
.map((r) => ({ apiName: String(r.Name ?? ""), label: String(r.Profile?.Name ?? "") }))
|
|
45
|
+
.filter((c) => c.apiName.length > 0);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Scope names the org answers to more than once, or answers to by the wrong name.
|
|
49
|
+
*
|
|
50
|
+
* Best-effort throughout — an ambiguity note is an improvement on silence, never a precondition for
|
|
51
|
+
* projecting. A failed enumeration returns nothing and the projection is unchanged.
|
|
52
|
+
*/
|
|
53
|
+
export async function checkScopeNameAmbiguity(reader, components) {
|
|
54
|
+
const wanted = [...new Set(components.filter((c) => c.typeName === "Profile").map((c) => c.fullName))].sort();
|
|
55
|
+
if (wanted.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
let candidates;
|
|
58
|
+
try {
|
|
59
|
+
candidates = await profileCandidates(reader);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (candidates.length === 0)
|
|
65
|
+
return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const requestedName of wanted) {
|
|
68
|
+
const matched = candidates.filter((c) => c.apiName === requestedName || c.label === requestedName);
|
|
69
|
+
// Nothing matched: that is plain absence, and the manifest's `found: false` already says it
|
|
70
|
+
// better than a note could.
|
|
71
|
+
if (matched.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
const byApiName = matched.filter((c) => c.apiName === requestedName);
|
|
74
|
+
// Exactly one candidate, matched by the name the read actually resolves on. Nothing to say.
|
|
75
|
+
if (matched.length === 1 && byApiName.length === 1)
|
|
76
|
+
continue;
|
|
77
|
+
const all = matched.map(describe).join(", ");
|
|
78
|
+
let matchedBy;
|
|
79
|
+
let note;
|
|
80
|
+
if (byApiName.length === 1) {
|
|
81
|
+
const other = matched.filter((c) => c.apiName !== requestedName).map(describe).join(", ");
|
|
82
|
+
matchedBy = "multiple";
|
|
83
|
+
note =
|
|
84
|
+
`${matched.length} profiles in this org answer to "${requestedName}": ${all}. ` +
|
|
85
|
+
`A Profile is read by API NAME, so the snapshot is ${describe(byApiName[0])} — ${other} was NOT captured. ` +
|
|
86
|
+
`Scope the API name explicitly to project the other one.`;
|
|
87
|
+
}
|
|
88
|
+
else if (byApiName.length === 0) {
|
|
89
|
+
matchedBy = "label";
|
|
90
|
+
note =
|
|
91
|
+
`No profile in this org has the API name "${requestedName}" — it is the LABEL of ${all}. ` +
|
|
92
|
+
`A Profile is read by API name, so this component reads as absent (or as some unrelated profile). ` +
|
|
93
|
+
`Scope it as ${matched.map((c) => `\`${c.apiName}\``).join(" or ")} instead.`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Two rows carrying the same API name. Which one the org handed back is not recoverable from
|
|
97
|
+
// the projection, and guessing would be worse than saying so.
|
|
98
|
+
matchedBy = "multiple";
|
|
99
|
+
note =
|
|
100
|
+
`${byApiName.length} profiles in this org share the API name "${requestedName}": ${all}. ` +
|
|
101
|
+
`Which one the read returned is NOT determinable from the projection — treat this snapshot as unattributed.`;
|
|
102
|
+
}
|
|
103
|
+
out.push({ typeName: "Profile", requestedName, matchedBy, candidates: matched, note });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Precedent and org traits — the two things every analyzer needs and no scope ever contains.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS IS STRUCTURAL AND NOT A SCOPE OPTION
|
|
4
|
+
//
|
|
5
|
+
// A projection is scoped to what the batch is BUILDING. On greenfield that set is empty by
|
|
6
|
+
// construction, which is exactly when precedent matters most: CF77's baseline held 2 components,
|
|
7
|
+
// both not-found, and CF78's held only the Admin profile. So the org-precedent lint tier
|
|
8
|
+
// (UNPROVEN_ORG_TOKEN) reported "no precedent to compare against" and the analyzers went and did
|
|
9
|
+
// the tool's job by hand — 21 tool calls over 10 minutes on CF77, 37 over 13 minutes on CF78,
|
|
10
|
+
// both hunting for utility-bar component tokens that were sitting in FlexiPages deployed to that
|
|
11
|
+
// same org.
|
|
12
|
+
//
|
|
13
|
+
// It cannot be a `precedentFor` parameter either. That works only when the supervisor remembers to
|
|
14
|
+
// pass it, and scoping to the objects under construction is the natural — and wrong — thing to do.
|
|
15
|
+
// The types that carry org-enforced free-string elements are a CLOSED SET the server already knows,
|
|
16
|
+
// so it collects them unconditionally and the tier cannot be scoped into silence.
|
|
17
|
+
//
|
|
18
|
+
// Cost is bounded and small: a capped sample per type, read through the same batched prefetch as
|
|
19
|
+
// everything else. We only need the VOCABULARY these components use, and a dozen instances carry
|
|
20
|
+
// essentially all of it.
|
|
21
|
+
/** Types carrying elements the ORG validates but no schema can — see ORG_ENFORCED_ELEMENTS. */
|
|
22
|
+
const PRECEDENT_TYPES = [
|
|
23
|
+
// Utility bars first: componentName is the element that has actually cost time, twice.
|
|
24
|
+
{ typeName: "FlexiPage", soql: "SELECT DeveloperName FROM FlexiPage WHERE Type = 'UtilityBar'", nameField: "DeveloperName" },
|
|
25
|
+
// Tooling CustomTab, NOT TabDefinition: TabDefinition is (a) a regular-API object, so the
|
|
26
|
+
// tooling query it used to run here failed on every org and this sweep silently collected
|
|
27
|
+
// nothing (live-caught CF79 — the only motif precedent in the projection was one the
|
|
28
|
+
// supervisor scoped by hand), and (b) a context-scoped UI-API view that returns empty for
|
|
29
|
+
// tabs that demonstrably exist. Tooling CustomTab hides DeveloperName for OBJECT tabs
|
|
30
|
+
// (returns null), so the filter keeps only named tabs — web/VF/Lightning tabs — which carry
|
|
31
|
+
// motifs just the same, and motif vocabulary is all this sweep is for.
|
|
32
|
+
{ typeName: "CustomTab", soql: "SELECT DeveloperName FROM CustomTab WHERE DeveloperName != null", nameField: "DeveloperName" }
|
|
33
|
+
];
|
|
34
|
+
// Enough to carry the vocabulary, few enough to stay cheap. CF78's org holds 80 utility bars; the
|
|
35
|
+
// distinct componentName set across the first dozen is the same as across all of them.
|
|
36
|
+
const SAMPLE_PER_TYPE = 12;
|
|
37
|
+
/**
|
|
38
|
+
* The types this sweep can sample — i.e. the only types whose org-enforced values precedent can ever
|
|
39
|
+
* prove.
|
|
40
|
+
*
|
|
41
|
+
* Exported because "cheap and bounded" turned out to be true per read and false in aggregate: a
|
|
42
|
+
* `direct_apply` reads its scope TWICE (pre and post), and across the recorded corpus these samples
|
|
43
|
+
* were 1,296 of 1,977 pre-projection component reads — 66% — against a median of 3 touched
|
|
44
|
+
* components per wave. A caller that reads its scope twice and knows its wave touches none of these
|
|
45
|
+
* types can skip the sweep entirely; `runProjection`'s `skipPrecedent` is how, and the lint tier the
|
|
46
|
+
* sweep feeds keys its lookup on the EDIT'S typeName (`<typeName>.<element>`), so a wave touching no
|
|
47
|
+
* type in this set could never have found a match here anyway.
|
|
48
|
+
*
|
|
49
|
+
* NOT a licence to scope it per-object — see the file header. The closed set is the point.
|
|
50
|
+
*/
|
|
51
|
+
export const PRECEDENT_TYPE_NAMES = new Set(PRECEDENT_TYPES.map((t) => t.typeName));
|
|
52
|
+
/**
|
|
53
|
+
* Facts about the ORG ITSELF that change what correct authoring looks like, and that no component
|
|
54
|
+
* read reveals.
|
|
55
|
+
*
|
|
56
|
+
* Live cost of not having them: on a multi-currency org every layout carries CurrencyIsoCode, and
|
|
57
|
+
* an authored Layout that omits it silently drops it — measured as real content loss. CF78 caught
|
|
58
|
+
* that at wave 4's pre-flight, which is late; the analyzer that wrote the file never knew. Each
|
|
59
|
+
* probe is a query that FAILS when the feature is off, so failure is the answer, not an error.
|
|
60
|
+
*/
|
|
61
|
+
export async function probeOrgTraits(reader) {
|
|
62
|
+
const notes = [];
|
|
63
|
+
// CurrencyType, RecordType and CallCenter are REGULAR-API objects. Routed through toolingQuery
|
|
64
|
+
// (the original shape) every probe failed on every org, so every trait read "no" forever —
|
|
65
|
+
// live-caught on CF79, where multi-currency reported "no" beside a field list carrying
|
|
66
|
+
// CurrencyIsoCode. A reader without soqlQuery gets "unavailable", never a fabricated negative.
|
|
67
|
+
const soql = reader.soqlQuery?.bind(reader);
|
|
68
|
+
if (!soql) {
|
|
69
|
+
notes.push("trait probes unavailable: reader has no regular-API SOQL — traits below are UNKNOWN, not measured; trust concrete evidence (e.g. CurrencyIsoCode in a field list) over these flags");
|
|
70
|
+
return { multiCurrency: false, personAccounts: false, callCenter: false, notes };
|
|
71
|
+
}
|
|
72
|
+
const probe = async (label, q) => {
|
|
73
|
+
try {
|
|
74
|
+
await soql(q);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// The sObject does not exist unless the feature is enabled, so this is a NEGATIVE result and
|
|
79
|
+
// not a failure. Recorded either way so a reader can tell "off" from "never asked".
|
|
80
|
+
notes.push(`${label}: not enabled (probe query rejected)`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const [multiCurrency, personAccounts, callCenter] = await Promise.all([
|
|
85
|
+
probe("multi-currency", "SELECT Id FROM CurrencyType LIMIT 1"),
|
|
86
|
+
probe("person accounts", "SELECT Id FROM RecordType WHERE IsPersonType = true LIMIT 1"),
|
|
87
|
+
probe("call center", "SELECT Id FROM CallCenter LIMIT 1")
|
|
88
|
+
]);
|
|
89
|
+
return { multiCurrency, personAccounts, callCenter, notes };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Existing components whose values prove what the org accepts.
|
|
93
|
+
*
|
|
94
|
+
* Best-effort throughout: precedent is an optimization, so a failed enumeration must never turn a
|
|
95
|
+
* projection that would have succeeded into one that fails. Anything not collected simply leaves
|
|
96
|
+
* the tier saying it has no precedent, which is the behaviour we already had.
|
|
97
|
+
*/
|
|
98
|
+
export async function collectPrecedentComponents(reader, alreadyScoped) {
|
|
99
|
+
const have = new Set(alreadyScoped.map((c) => `${c.typeName}:${c.fullName}`));
|
|
100
|
+
// Prefer instances from the same NAMING FAMILY as the scoped work before alphabetical fill.
|
|
101
|
+
// A purely alphabetical sample of CF79's org spent all 12 FlexiPage slots on Accounting/BTS
|
|
102
|
+
// bars and never picked up CF71–CF78_Operations_Console_UtilityBar — the direct ancestors of
|
|
103
|
+
// the bar being authored, deployed by prior runs of this very loop. Family = the leading
|
|
104
|
+
// alpha token of a prefixed name (CF79_… -> CF), so successive batches find their lineage.
|
|
105
|
+
const families = new Set();
|
|
106
|
+
for (const c of alreadyScoped) {
|
|
107
|
+
const m = /^([A-Za-z]+)\d/.exec(c.fullName);
|
|
108
|
+
if (m)
|
|
109
|
+
families.add(m[1].toUpperCase());
|
|
110
|
+
}
|
|
111
|
+
const inFamily = (n) => {
|
|
112
|
+
const m = /^([A-Za-z]+)\d/.exec(n);
|
|
113
|
+
return m ? families.has(m[1].toUpperCase()) : false;
|
|
114
|
+
};
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const { typeName, soql, nameField } of PRECEDENT_TYPES) {
|
|
117
|
+
try {
|
|
118
|
+
const rows = await reader.toolingQuery(soql);
|
|
119
|
+
const names = rows.records
|
|
120
|
+
.map((r) => String(r[nameField] ?? ""))
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
// Deterministic sample: family instances first, alphabetical inside each half, so two
|
|
123
|
+
// runs of the same org with the same scope pick the same instances and the manifest
|
|
124
|
+
// stays comparable across projections.
|
|
125
|
+
.sort((a, b) => Number(inFamily(b)) - Number(inFamily(a)) || a.localeCompare(b))
|
|
126
|
+
.filter((n) => !have.has(`${typeName}:${n}`))
|
|
127
|
+
.slice(0, SAMPLE_PER_TYPE);
|
|
128
|
+
for (const fullName of names)
|
|
129
|
+
out.push({ typeName, fullName });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* enumeration unavailable — no precedent for this type, same as before */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|