feat(inventory): enforce high-risk contextual help
This commit is contained in:
@@ -63,6 +63,28 @@ const helpAttributes = new Set([
|
||||
"helperText",
|
||||
"helpText"
|
||||
]);
|
||||
const exactHelpAttributes = new Set([
|
||||
"data-help-context-id",
|
||||
"helpContextId"
|
||||
]);
|
||||
const helpRiskAttributes = new Set([
|
||||
"data-help-risk",
|
||||
"helpRisk"
|
||||
]);
|
||||
const reviewedHelpRiskAttributes = new Set([
|
||||
"data-help-risk-reviewed",
|
||||
"helpRiskReviewed"
|
||||
]);
|
||||
const supportedHelpRisks = new Set([
|
||||
"authority",
|
||||
"credential",
|
||||
"disclosure",
|
||||
"encryption",
|
||||
"external-effect",
|
||||
"irreversible",
|
||||
"policy",
|
||||
"retention"
|
||||
]);
|
||||
const actionComponentPattern = /(?:Action|Button|Link)$/;
|
||||
const contributionTypes = new Map([
|
||||
["AdminSectionsUiCapability", "admin_section"],
|
||||
@@ -200,20 +222,43 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
const parentAttributes = parentFormField
|
||||
? jsxAttributes(parentFormField)
|
||||
: new Map();
|
||||
const scopedAncestorAttributes = nearestScopedHelpAttributes(node);
|
||||
const label =
|
||||
attributes.get("label") ??
|
||||
attributes.get("aria-label") ??
|
||||
parentAttributes.get("label") ??
|
||||
null;
|
||||
const help = firstAttribute(attributes, helpAttributes) ??
|
||||
firstAttribute(parentAttributes, helpAttributes);
|
||||
firstAttribute(parentAttributes, helpAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, helpAttributes);
|
||||
const hasHelp = hasAnyAttribute(attributes, helpAttributes) ||
|
||||
hasAnyAttribute(parentAttributes, helpAttributes);
|
||||
hasAnyAttribute(parentAttributes, helpAttributes) ||
|
||||
hasAnyAttribute(scopedAncestorAttributes, helpAttributes);
|
||||
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes) ||
|
||||
hasAnyAttribute(parentAttributes, exactHelpAttributes) ||
|
||||
hasAnyAttribute(scopedAncestorAttributes, exactHelpAttributes);
|
||||
const helpContextId = firstAttribute(attributes, exactHelpAttributes) ??
|
||||
firstAttribute(parentAttributes, exactHelpAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, exactHelpAttributes);
|
||||
const explicitId = firstAttribute(
|
||||
attributes,
|
||||
new Set(["interfaceId", "data-interface-id", "id", "name", "field"])
|
||||
);
|
||||
const context = nearestNamedContext(node);
|
||||
const risk = helpRiskFor({
|
||||
component,
|
||||
context,
|
||||
file: relativeFile,
|
||||
label,
|
||||
explicitId,
|
||||
name: attributes.get("name") ?? attributes.get("id") ?? attributes.get("field") ?? null,
|
||||
explicitRisk: firstAttribute(attributes, helpRiskAttributes) ??
|
||||
firstAttribute(parentAttributes, helpRiskAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, helpRiskAttributes)
|
||||
});
|
||||
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes) ??
|
||||
firstAttribute(parentAttributes, reviewedHelpRiskAttributes) ??
|
||||
firstAttribute(scopedAncestorAttributes, reviewedHelpRiskAttributes);
|
||||
const stableId = sourceIdentity(
|
||||
"field",
|
||||
node,
|
||||
@@ -237,7 +282,14 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
help: help ?? null,
|
||||
helpId: hasHelp ? `${stableId}.help` : null,
|
||||
helpDynamic: hasHelp && help === null,
|
||||
helpCandidate: !hasHelp
|
||||
helpCandidate: !hasHelp,
|
||||
helpExact: hasExactHelp,
|
||||
helpContextId,
|
||||
helpContextDynamic: hasExactHelp && helpContextId === null,
|
||||
helpRisk: risk.value,
|
||||
helpRiskSource: risk.source,
|
||||
helpRiskReviewed: riskReviewed,
|
||||
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
|
||||
});
|
||||
|
||||
}
|
||||
@@ -261,6 +313,19 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
new Set(["interfaceId", "data-interface-id", "id", "name"])
|
||||
);
|
||||
const context = nearestNamedContext(node);
|
||||
const hasHelp = hasAnyAttribute(attributes, helpAttributes);
|
||||
const hasExactHelp = hasAnyAttribute(attributes, exactHelpAttributes);
|
||||
const helpContextId = firstAttribute(attributes, exactHelpAttributes);
|
||||
const risk = helpRiskFor({
|
||||
component,
|
||||
context,
|
||||
file: relativeFile,
|
||||
label,
|
||||
explicitId,
|
||||
name: attributes.get("name") ?? attributes.get("id") ?? null,
|
||||
explicitRisk: firstAttribute(attributes, helpRiskAttributes)
|
||||
});
|
||||
const riskReviewed = firstAttribute(attributes, reviewedHelpRiskAttributes);
|
||||
result.actions.push({
|
||||
...locate(node),
|
||||
id: sourceIdentity(
|
||||
@@ -273,7 +338,15 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
idSource: explicitId === null ? "source_anchor" : "explicit",
|
||||
context,
|
||||
component,
|
||||
label
|
||||
label,
|
||||
helpExact: hasExactHelp,
|
||||
helpContextId,
|
||||
helpContextDynamic: hasExactHelp && helpContextId === null,
|
||||
helpDynamic: hasHelp && firstAttribute(attributes, helpAttributes) === null,
|
||||
helpRisk: risk.value,
|
||||
helpRiskSource: risk.source,
|
||||
helpRiskReviewed: riskReviewed,
|
||||
highRiskHelpMissing: risk.value !== null && !hasExactHelp && riskReviewed !== "standard"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -354,6 +427,26 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function nearestScopedHelpAttributes(node) {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (ts.isJsxElement(current)) {
|
||||
const attributes = jsxAttributes(current.openingElement);
|
||||
if (attributes.get("data-help-scope") === "field") return attributes;
|
||||
}
|
||||
if (
|
||||
ts.isFunctionDeclaration(current) ||
|
||||
ts.isMethodDeclaration(current) ||
|
||||
ts.isArrowFunction(current) ||
|
||||
ts.isFunctionExpression(current)
|
||||
) {
|
||||
return new Map();
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
function jsxAttributes(node) {
|
||||
const mapped = new Map();
|
||||
for (const attribute of node.attributes.properties) {
|
||||
@@ -579,6 +672,34 @@ function hasAnyAttribute(attributes, names) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function helpRiskFor({ component, context, file, label, explicitId, name, explicitRisk }) {
|
||||
if (typeof explicitRisk === "string") {
|
||||
return supportedHelpRisks.has(explicitRisk)
|
||||
? { value: explicitRisk, source: "explicit" }
|
||||
: { value: null, source: "invalid_explicit" };
|
||||
}
|
||||
const value = [component, context, file, label, explicitId, name]
|
||||
.filter((item) => typeof item === "string")
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.replace(/^i18n:/g, "")
|
||||
.replace(/[._-]+/g, " ");
|
||||
const patterns = [
|
||||
["irreversible", /\b(delete|destroy|erase|purge|dispose|disposition|revoke|withdraw|shred)\b/],
|
||||
["credential", /\b(credential|password|secret|token|api key|private key)\b/],
|
||||
["retention", /\b(retention|legal hold|archive lifecycle)\b/],
|
||||
["encryption", /\b(encrypt|encryption|decrypt|decryption|signing key|signature key)\b/],
|
||||
["disclosure", /\b(disclose|disclosure|publish|share externally|public export)\b/],
|
||||
["external-effect", /\b(send|deliver|transfer|refund|payment execution|webhook execution)\b/],
|
||||
["authority", /\b(grant permission|role assignment|approve|reject|formal decision|mandate)\b/],
|
||||
["policy", /\b(policy apply|policy override|enforcement mode)\b/]
|
||||
];
|
||||
for (const [risk, pattern] of patterns) {
|
||||
if (pattern.test(value)) return { value: risk, source: "inferred" };
|
||||
}
|
||||
return { value: null, source: null };
|
||||
}
|
||||
|
||||
function slug(value) {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
|
||||
Reference in New Issue
Block a user