feat(inventory): enforce high-risk contextual help
This commit is contained in:
@@ -4,46 +4,46 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan"
|
name = "govoplan"
|
||||||
version = "0.1.38"
|
version = "0.1.39"
|
||||||
description = "Developer convenience package for a versioned GovOPlaN composition"
|
description = "Developer convenience package for a versioned GovOPlaN composition"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { text = "AGPL-3.0-or-later" }
|
license = { text = "AGPL-3.0-or-later" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core[server]==0.1.38",
|
"govoplan-core[server]==0.1.39",
|
||||||
"govoplan-tenancy==0.1.20",
|
"govoplan-tenancy==0.1.20",
|
||||||
"govoplan-organizations==0.1.19",
|
"govoplan-organizations==0.1.20",
|
||||||
"govoplan-identity==0.1.20",
|
"govoplan-identity==0.1.20",
|
||||||
"govoplan-idm==0.1.23",
|
"govoplan-idm==0.1.24",
|
||||||
"govoplan-access==0.1.22",
|
"govoplan-access==0.1.23",
|
||||||
"govoplan-admin==0.1.21",
|
"govoplan-admin==0.1.22",
|
||||||
"govoplan-policy==0.1.22",
|
"govoplan-policy==0.1.22",
|
||||||
"govoplan-audit==0.1.20",
|
"govoplan-audit==0.1.20",
|
||||||
"govoplan-dashboard==0.1.20",
|
"govoplan-dashboard==0.1.20",
|
||||||
"govoplan-files==0.1.22",
|
"govoplan-files==0.1.23",
|
||||||
"govoplan-mail==0.1.23",
|
"govoplan-mail==0.1.24",
|
||||||
"govoplan-campaign==0.1.26",
|
"govoplan-campaign==0.1.27",
|
||||||
"govoplan-calendar==0.1.21",
|
"govoplan-calendar==0.1.22",
|
||||||
"govoplan-docs==0.1.21",
|
"govoplan-docs==0.1.22",
|
||||||
"govoplan-ops==0.1.20",
|
"govoplan-ops==0.1.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
full = [
|
full = [
|
||||||
"govoplan-addresses==0.1.20",
|
"govoplan-addresses==0.1.21",
|
||||||
"govoplan-approvals==0.1.19",
|
"govoplan-approvals==0.1.20",
|
||||||
"govoplan-assets==0.1.20",
|
"govoplan-assets==0.1.20",
|
||||||
"govoplan-booking==0.1.20",
|
"govoplan-booking==0.1.20",
|
||||||
"govoplan-cases==0.1.22",
|
"govoplan-cases==0.1.22",
|
||||||
"govoplan-certificates==0.1.20",
|
"govoplan-certificates==0.1.20",
|
||||||
"govoplan-committee==0.1.19",
|
"govoplan-committee==0.1.20",
|
||||||
"govoplan-connectors==0.1.24",
|
"govoplan-connectors==0.1.25",
|
||||||
"govoplan-consultation==0.1.20",
|
"govoplan-consultation==0.1.20",
|
||||||
"govoplan-contracts==0.1.20",
|
"govoplan-contracts==0.1.20",
|
||||||
"govoplan-dataflow==0.1.22",
|
"govoplan-dataflow==0.1.23",
|
||||||
"govoplan-datasources==0.1.23",
|
"govoplan-datasources==0.1.24",
|
||||||
"govoplan-decisions==0.1.19",
|
"govoplan-decisions==0.1.19",
|
||||||
"govoplan-dist-lists==0.1.19",
|
"govoplan-dist-lists==0.1.20",
|
||||||
"govoplan-dms==0.1.20",
|
"govoplan-dms==0.1.20",
|
||||||
"govoplan-encryption==0.1.19",
|
"govoplan-encryption==0.1.19",
|
||||||
"govoplan-erp==0.1.20",
|
"govoplan-erp==0.1.20",
|
||||||
@@ -54,7 +54,7 @@ full = [
|
|||||||
"govoplan-forms-runtime==0.1.19",
|
"govoplan-forms-runtime==0.1.19",
|
||||||
"govoplan-grants==0.1.20",
|
"govoplan-grants==0.1.20",
|
||||||
"govoplan-helpdesk==0.1.21",
|
"govoplan-helpdesk==0.1.21",
|
||||||
"govoplan-identity-trust==0.1.19",
|
"govoplan-identity-trust==0.1.20",
|
||||||
"govoplan-inspections==0.1.20",
|
"govoplan-inspections==0.1.20",
|
||||||
"govoplan-learning==0.1.20",
|
"govoplan-learning==0.1.20",
|
||||||
"govoplan-mandates==0.1.19",
|
"govoplan-mandates==0.1.19",
|
||||||
@@ -63,28 +63,28 @@ full = [
|
|||||||
"govoplan-payments==0.1.21",
|
"govoplan-payments==0.1.21",
|
||||||
"govoplan-permits==0.1.20",
|
"govoplan-permits==0.1.20",
|
||||||
"govoplan-poll==0.1.20",
|
"govoplan-poll==0.1.20",
|
||||||
"govoplan-portal==0.1.20",
|
"govoplan-portal==0.1.21",
|
||||||
"govoplan-postbox==0.1.20",
|
"govoplan-postbox==0.1.21",
|
||||||
"govoplan-procurement==0.1.20",
|
"govoplan-procurement==0.1.20",
|
||||||
"govoplan-projects==0.1.19",
|
"govoplan-projects==0.1.19",
|
||||||
"govoplan-quick-access==0.1.20",
|
"govoplan-quick-access==0.1.20",
|
||||||
"govoplan-records==0.1.21",
|
"govoplan-records==0.1.22",
|
||||||
"govoplan-reporting==0.1.19",
|
"govoplan-reporting==0.1.20",
|
||||||
"govoplan-resources==0.1.20",
|
"govoplan-resources==0.1.20",
|
||||||
"govoplan-rest==0.1.19",
|
"govoplan-rest==0.1.19",
|
||||||
"govoplan-risk-compliance==0.1.19",
|
"govoplan-risk-compliance==0.1.20",
|
||||||
"govoplan-scheduling==0.1.21",
|
"govoplan-scheduling==0.1.21",
|
||||||
"govoplan-search==0.1.19",
|
"govoplan-search==0.1.19",
|
||||||
"govoplan-services==0.1.19",
|
"govoplan-services==0.1.19",
|
||||||
"govoplan-soap==0.1.19",
|
"govoplan-soap==0.1.19",
|
||||||
"govoplan-tasks==0.1.21",
|
"govoplan-tasks==0.1.21",
|
||||||
"govoplan-templates==0.1.20",
|
"govoplan-templates==0.1.21",
|
||||||
"govoplan-tickets==0.1.21",
|
"govoplan-tickets==0.1.22",
|
||||||
"govoplan-transparency==0.1.20",
|
"govoplan-transparency==0.1.20",
|
||||||
"govoplan-views==0.1.20",
|
"govoplan-views==0.1.21",
|
||||||
"govoplan-voting==0.1.20",
|
"govoplan-voting==0.1.20",
|
||||||
"govoplan-wiki==0.1.21",
|
"govoplan-wiki==0.1.22",
|
||||||
"govoplan-workflow==0.1.21",
|
"govoplan-workflow==0.1.22",
|
||||||
"govoplan-workflow-engine==0.1.21",
|
"govoplan-workflow-engine==0.1.21",
|
||||||
"govoplan-xrechnung==0.1.21",
|
"govoplan-xrechnung==0.1.21",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
# Only add a module after its referenced tag has been published.
|
# Only add a module after its referenced tag has been published.
|
||||||
../govoplan-core[server]
|
../govoplan-core[server]
|
||||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.20
|
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.20
|
||||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.19
|
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.20
|
||||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
|
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
|
||||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.23
|
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.24
|
||||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.22
|
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.23
|
||||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.21
|
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.22
|
||||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.22
|
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.22
|
||||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
|
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
|
||||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
|
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
|
||||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.22
|
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.23
|
||||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.23
|
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.24
|
||||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.26
|
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.27
|
||||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.21
|
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.22
|
||||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.21
|
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.22
|
||||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.20
|
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.20
|
||||||
|
|||||||
@@ -169,6 +169,69 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_high_risk_help_baseline_is_validated(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / "help-baseline.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"maximum_missing_exact_help": 3,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
3,
|
||||||
|
inventory._load_high_risk_help_baseline(path)[
|
||||||
|
"maximum_missing_exact_help"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"maximum_missing_exact_help": -1,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "non-negative integer"):
|
||||||
|
inventory._load_high_risk_help_baseline(path)
|
||||||
|
|
||||||
|
def test_declaration_strict_mode_rejects_high_risk_help_regression(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
result = {
|
||||||
|
"translation_health": {"missing_catalog_entries": []},
|
||||||
|
"api": {
|
||||||
|
"unclassified_endpoints": [],
|
||||||
|
"stale_endpoint_declarations": [],
|
||||||
|
},
|
||||||
|
"declaration_health": {},
|
||||||
|
"help_health": {
|
||||||
|
"invalid_risk_annotations": [],
|
||||||
|
"unresolved_exact_high_risk_help": [],
|
||||||
|
"high_risk_help_without_german": [],
|
||||||
|
"missing_exact_high_risk_help": [{"id": "example.delete"}],
|
||||||
|
"baseline_maximum_missing": 0,
|
||||||
|
"baseline_regression": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"1 high-risk controls lack exact F1 help; baseline permits at most 0"
|
||||||
|
],
|
||||||
|
inventory._strict_failures(
|
||||||
|
result,
|
||||||
|
check_translations=False,
|
||||||
|
check_endpoints=False,
|
||||||
|
check_declarations=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
|
def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
|
||||||
tree = ast.parse(
|
tree = ast.parse(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -63,6 +63,28 @@ const helpAttributes = new Set([
|
|||||||
"helperText",
|
"helperText",
|
||||||
"helpText"
|
"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 actionComponentPattern = /(?:Action|Button|Link)$/;
|
||||||
const contributionTypes = new Map([
|
const contributionTypes = new Map([
|
||||||
["AdminSectionsUiCapability", "admin_section"],
|
["AdminSectionsUiCapability", "admin_section"],
|
||||||
@@ -200,20 +222,43 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
|||||||
const parentAttributes = parentFormField
|
const parentAttributes = parentFormField
|
||||||
? jsxAttributes(parentFormField)
|
? jsxAttributes(parentFormField)
|
||||||
: new Map();
|
: new Map();
|
||||||
|
const scopedAncestorAttributes = nearestScopedHelpAttributes(node);
|
||||||
const label =
|
const label =
|
||||||
attributes.get("label") ??
|
attributes.get("label") ??
|
||||||
attributes.get("aria-label") ??
|
attributes.get("aria-label") ??
|
||||||
parentAttributes.get("label") ??
|
parentAttributes.get("label") ??
|
||||||
null;
|
null;
|
||||||
const help = firstAttribute(attributes, helpAttributes) ??
|
const help = firstAttribute(attributes, helpAttributes) ??
|
||||||
firstAttribute(parentAttributes, helpAttributes);
|
firstAttribute(parentAttributes, helpAttributes) ??
|
||||||
|
firstAttribute(scopedAncestorAttributes, helpAttributes);
|
||||||
const hasHelp = hasAnyAttribute(attributes, 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(
|
const explicitId = firstAttribute(
|
||||||
attributes,
|
attributes,
|
||||||
new Set(["interfaceId", "data-interface-id", "id", "name", "field"])
|
new Set(["interfaceId", "data-interface-id", "id", "name", "field"])
|
||||||
);
|
);
|
||||||
const context = nearestNamedContext(node);
|
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(
|
const stableId = sourceIdentity(
|
||||||
"field",
|
"field",
|
||||||
node,
|
node,
|
||||||
@@ -237,7 +282,14 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
|||||||
help: help ?? null,
|
help: help ?? null,
|
||||||
helpId: hasHelp ? `${stableId}.help` : null,
|
helpId: hasHelp ? `${stableId}.help` : null,
|
||||||
helpDynamic: hasHelp && 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"])
|
new Set(["interfaceId", "data-interface-id", "id", "name"])
|
||||||
);
|
);
|
||||||
const context = nearestNamedContext(node);
|
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({
|
result.actions.push({
|
||||||
...locate(node),
|
...locate(node),
|
||||||
id: sourceIdentity(
|
id: sourceIdentity(
|
||||||
@@ -273,7 +338,15 @@ function inspectSource(repository, sourceRoot, sourcePath) {
|
|||||||
idSource: explicitId === null ? "source_anchor" : "explicit",
|
idSource: explicitId === null ? "source_anchor" : "explicit",
|
||||||
context,
|
context,
|
||||||
component,
|
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;
|
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) {
|
function jsxAttributes(node) {
|
||||||
const mapped = new Map();
|
const mapped = new Map();
|
||||||
for (const attribute of node.attributes.properties) {
|
for (const attribute of node.attributes.properties) {
|
||||||
@@ -579,6 +672,34 @@ function hasAnyAttribute(attributes, names) {
|
|||||||
return false;
|
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) {
|
function slug(value) {
|
||||||
const normalized = value
|
const normalized = value
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"maximum_missing_exact_help": 0,
|
||||||
|
"rationale": "The source-derived high-risk queue for Core issue #284 is fully resolved. Strict declarations reject any new high-risk control without an exact, manifest-declared, German-complete F1 context."
|
||||||
|
}
|
||||||
@@ -31,6 +31,9 @@ ENDPOINT_SURFACE_CATEGORIES = {
|
|||||||
DEFAULT_ENDPOINT_DECLARATIONS = (
|
DEFAULT_ENDPOINT_DECLARATIONS = (
|
||||||
META_ROOT / "tools" / "inventory" / "endpoint-surface-declarations.json"
|
META_ROOT / "tools" / "inventory" / "endpoint-surface-declarations.json"
|
||||||
)
|
)
|
||||||
|
DEFAULT_HIGH_RISK_HELP_BASELINE = (
|
||||||
|
META_ROOT / "tools" / "inventory" / "high-risk-help-baseline.json"
|
||||||
|
)
|
||||||
REQUIRED_LOCALES = ("de", "en")
|
REQUIRED_LOCALES = ("de", "en")
|
||||||
REFERENCE_LOCALE = "de"
|
REFERENCE_LOCALE = "de"
|
||||||
|
|
||||||
@@ -75,6 +78,12 @@ def main() -> int:
|
|||||||
default=DEFAULT_ENDPOINT_DECLARATIONS,
|
default=DEFAULT_ENDPOINT_DECLARATIONS,
|
||||||
help="Versioned endpoint-surface declaration registry.",
|
help="Versioned endpoint-surface declaration registry.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--high-risk-help-baseline",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_HIGH_RISK_HELP_BASELINE,
|
||||||
|
help="Versioned upper bound for high-risk controls without exact F1 help.",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||||
@@ -85,11 +94,15 @@ def main() -> int:
|
|||||||
endpoint_declarations = _load_endpoint_declarations(
|
endpoint_declarations = _load_endpoint_declarations(
|
||||||
args.endpoint_declarations.resolve()
|
args.endpoint_declarations.resolve()
|
||||||
)
|
)
|
||||||
|
high_risk_help_baseline = _load_high_risk_help_baseline(
|
||||||
|
args.high_risk_help_baseline.resolve()
|
||||||
|
)
|
||||||
inventory = _assemble_inventory(
|
inventory = _assemble_inventory(
|
||||||
webui=webui,
|
webui=webui,
|
||||||
backend_endpoints=backend_endpoints,
|
backend_endpoints=backend_endpoints,
|
||||||
manifests=manifests,
|
manifests=manifests,
|
||||||
endpoint_declarations=endpoint_declarations,
|
endpoint_declarations=endpoint_declarations,
|
||||||
|
high_risk_help_baseline=high_risk_help_baseline,
|
||||||
runtime_snapshot=(
|
runtime_snapshot=(
|
||||||
_load_runtime_snapshot(args.runtime_snapshot.resolve())
|
_load_runtime_snapshot(args.runtime_snapshot.resolve())
|
||||||
if args.runtime_snapshot is not None
|
if args.runtime_snapshot is not None
|
||||||
@@ -164,6 +177,28 @@ def _strict_failures(
|
|||||||
f"{len(declaration_health['stale_runtime_routes'])} runtime route "
|
f"{len(declaration_health['stale_runtime_routes'])} runtime route "
|
||||||
"declarations have no WebUI implementation"
|
"declarations have no WebUI implementation"
|
||||||
)
|
)
|
||||||
|
help_health = inventory.get("help_health", {})
|
||||||
|
if check_declarations and help_health.get("invalid_risk_annotations"):
|
||||||
|
failures.append(
|
||||||
|
f"{len(help_health['invalid_risk_annotations'])} controls use an "
|
||||||
|
"unsupported contextual-help risk class"
|
||||||
|
)
|
||||||
|
if check_declarations and help_health.get("baseline_regression"):
|
||||||
|
failures.append(
|
||||||
|
f"{len(help_health['missing_exact_high_risk_help'])} high-risk "
|
||||||
|
"controls lack exact F1 help; baseline permits at most "
|
||||||
|
f"{help_health['baseline_maximum_missing']}"
|
||||||
|
)
|
||||||
|
if check_declarations and help_health.get("unresolved_exact_high_risk_help"):
|
||||||
|
failures.append(
|
||||||
|
f"{len(help_health['unresolved_exact_high_risk_help'])} high-risk "
|
||||||
|
"controls reference no manifest DocumentationTopic help context"
|
||||||
|
)
|
||||||
|
if check_declarations and help_health.get("high_risk_help_without_german"):
|
||||||
|
failures.append(
|
||||||
|
f"{len(help_health['high_risk_help_without_german'])} high-risk "
|
||||||
|
"controls resolve to documentation without complete German content"
|
||||||
|
)
|
||||||
runtime_comparison = inventory.get("runtime_comparison")
|
runtime_comparison = inventory.get("runtime_comparison")
|
||||||
if (
|
if (
|
||||||
check_declarations
|
check_declarations
|
||||||
@@ -355,6 +390,29 @@ def _extract_manifests(
|
|||||||
}
|
}
|
||||||
for permission in manifest.permissions
|
for permission in manifest.permissions
|
||||||
],
|
],
|
||||||
|
"documentation": [
|
||||||
|
{
|
||||||
|
"id": topic.id,
|
||||||
|
"help_contexts": sorted(
|
||||||
|
{
|
||||||
|
str(context)
|
||||||
|
for context in topic.metadata.get(
|
||||||
|
"help_contexts", ()
|
||||||
|
)
|
||||||
|
if isinstance(context, str) and context.strip()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"german_complete": (
|
||||||
|
isinstance(topic.translations.get("de"), dict)
|
||||||
|
and all(
|
||||||
|
isinstance(topic.translations["de"].get(field), str)
|
||||||
|
and topic.translations["de"][field].strip()
|
||||||
|
for field in ("title", "summary", "body")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for topic in manifest.documentation
|
||||||
|
],
|
||||||
"architecture": (
|
"architecture": (
|
||||||
manifest.architecture.to_dict()
|
manifest.architecture.to_dict()
|
||||||
if manifest.architecture is not None
|
if manifest.architecture is not None
|
||||||
@@ -400,6 +458,7 @@ def _assemble_inventory(
|
|||||||
backend_endpoints: list[dict[str, Any]],
|
backend_endpoints: list[dict[str, Any]],
|
||||||
manifests: list[dict[str, Any]],
|
manifests: list[dict[str, Any]],
|
||||||
endpoint_declarations: dict[tuple[str, str, str], dict[str, Any]],
|
endpoint_declarations: dict[tuple[str, str, str], dict[str, Any]],
|
||||||
|
high_risk_help_baseline: dict[str, Any] | None = None,
|
||||||
runtime_snapshot: dict[str, Any] | None = None,
|
runtime_snapshot: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
frontend_refs = webui["frontendApiReferences"]
|
frontend_refs = webui["frontendApiReferences"]
|
||||||
@@ -471,8 +530,47 @@ def _assemble_inventory(
|
|||||||
if any(key not in catalog_keys.get(locale, set()) for locale in expected_locales)
|
if any(key not in catalog_keys.get(locale, set()) for locale in expected_locales)
|
||||||
]
|
]
|
||||||
fields = webui["fields"]
|
fields = webui["fields"]
|
||||||
|
actions = webui.get("actions", [])
|
||||||
help_candidates = [field for field in fields if field["helpCandidate"]]
|
help_candidates = [field for field in fields if field["helpCandidate"]]
|
||||||
dynamic_help = [field for field in fields if field.get("helpDynamic")]
|
dynamic_help = [field for field in fields if field.get("helpDynamic")]
|
||||||
|
controls = [*fields, *actions]
|
||||||
|
high_risk_controls = [item for item in controls if item.get("helpRisk")]
|
||||||
|
missing_exact_high_risk_help = [
|
||||||
|
item for item in controls if item.get("highRiskHelpMissing")
|
||||||
|
]
|
||||||
|
invalid_risk_annotations = [
|
||||||
|
item
|
||||||
|
for item in controls
|
||||||
|
if item.get("helpRiskSource") == "invalid_explicit"
|
||||||
|
]
|
||||||
|
documentation_contexts = {
|
||||||
|
context: {
|
||||||
|
"module_id": manifest["id"],
|
||||||
|
"topic_id": topic["id"],
|
||||||
|
"german_complete": topic["german_complete"],
|
||||||
|
}
|
||||||
|
for manifest in manifests
|
||||||
|
for topic in manifest.get("documentation", [])
|
||||||
|
for context in topic.get("help_contexts", [])
|
||||||
|
}
|
||||||
|
unresolved_exact_high_risk_help = [
|
||||||
|
item
|
||||||
|
for item in high_risk_controls
|
||||||
|
if item.get("helpExact")
|
||||||
|
and not item.get("helpContextDynamic")
|
||||||
|
and item.get("helpContextId") not in documentation_contexts
|
||||||
|
]
|
||||||
|
high_risk_help_without_german = [
|
||||||
|
item
|
||||||
|
for item in high_risk_controls
|
||||||
|
if item.get("helpContextId") in documentation_contexts
|
||||||
|
and not documentation_contexts[item["helpContextId"]]["german_complete"]
|
||||||
|
]
|
||||||
|
baseline_maximum_missing = (
|
||||||
|
high_risk_help_baseline["maximum_missing_exact_help"]
|
||||||
|
if high_risk_help_baseline is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
governance_adoption = Counter(
|
governance_adoption = Counter(
|
||||||
dimension["adoption"]
|
dimension["adoption"]
|
||||||
for manifest in manifests
|
for manifest in manifests
|
||||||
@@ -499,10 +597,34 @@ def _assemble_inventory(
|
|||||||
"modules": manifests,
|
"modules": manifests,
|
||||||
"interface_declarations": source_declarations,
|
"interface_declarations": source_declarations,
|
||||||
"declaration_health": declaration_health,
|
"declaration_health": declaration_health,
|
||||||
|
"help_health": {
|
||||||
|
"supported_risk_classes": sorted(
|
||||||
|
{
|
||||||
|
str(item["helpRisk"])
|
||||||
|
for item in high_risk_controls
|
||||||
|
if item.get("helpRisk")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"high_risk_controls": high_risk_controls,
|
||||||
|
"missing_exact_high_risk_help": missing_exact_high_risk_help,
|
||||||
|
"invalid_risk_annotations": invalid_risk_annotations,
|
||||||
|
"unresolved_exact_high_risk_help": unresolved_exact_high_risk_help,
|
||||||
|
"high_risk_help_without_german": high_risk_help_without_german,
|
||||||
|
"dynamic_owner_context_controls": [
|
||||||
|
item
|
||||||
|
for item in high_risk_controls
|
||||||
|
if item.get("helpContextDynamic")
|
||||||
|
],
|
||||||
|
"baseline_maximum_missing": baseline_maximum_missing,
|
||||||
|
"baseline_regression": (
|
||||||
|
baseline_maximum_missing is not None
|
||||||
|
and len(missing_exact_high_risk_help) > baseline_maximum_missing
|
||||||
|
),
|
||||||
|
},
|
||||||
"runtime_comparison": runtime_comparison,
|
"runtime_comparison": runtime_comparison,
|
||||||
"ui": {
|
"ui": {
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
"actions": webui.get("actions", []),
|
"actions": actions,
|
||||||
"labels": webui["labels"],
|
"labels": webui["labels"],
|
||||||
"visible_text": webui["visibleText"],
|
"visible_text": webui["visibleText"],
|
||||||
"routes": webui["routes"],
|
"routes": webui["routes"],
|
||||||
@@ -554,7 +676,19 @@ def _assemble_inventory(
|
|||||||
"ui_fields_with_resolvable_f1_context": len(fields),
|
"ui_fields_with_resolvable_f1_context": len(fields),
|
||||||
"help_review_candidates": len(help_candidates),
|
"help_review_candidates": len(help_candidates),
|
||||||
"dynamic_help_references": len(dynamic_help),
|
"dynamic_help_references": len(dynamic_help),
|
||||||
"ui_actions": len(webui.get("actions", [])),
|
"ui_actions": len(actions),
|
||||||
|
"high_risk_controls": len(high_risk_controls),
|
||||||
|
"high_risk_controls_with_exact_help": (
|
||||||
|
len(high_risk_controls) - len(missing_exact_high_risk_help)
|
||||||
|
),
|
||||||
|
"high_risk_controls_missing_exact_help": len(
|
||||||
|
missing_exact_high_risk_help
|
||||||
|
),
|
||||||
|
"invalid_help_risk_annotations": len(invalid_risk_annotations),
|
||||||
|
"unresolved_exact_high_risk_help": len(
|
||||||
|
unresolved_exact_high_risk_help
|
||||||
|
),
|
||||||
|
"high_risk_help_without_german": len(high_risk_help_without_german),
|
||||||
"interface_declarations": len(source_declarations),
|
"interface_declarations": len(source_declarations),
|
||||||
"duplicate_interface_ids": len(declaration_health["duplicate_ids"]),
|
"duplicate_interface_ids": len(declaration_health["duplicate_ids"]),
|
||||||
"undeclared_source_surfaces": len(
|
"undeclared_source_surfaces": len(
|
||||||
@@ -885,6 +1019,10 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
|
|||||||
for item in inventory["api"]["unreferenced_by_static_webui_scan"]
|
for item in inventory["api"]["unreferenced_by_static_webui_scan"]
|
||||||
)
|
)
|
||||||
classification_counts = inventory["api"]["classification_counts"]
|
classification_counts = inventory["api"]["classification_counts"]
|
||||||
|
high_risk_by_repository = Counter(
|
||||||
|
item["repository"]
|
||||||
|
for item in inventory["help_health"]["missing_exact_high_risk_help"]
|
||||||
|
)
|
||||||
lines = [
|
lines = [
|
||||||
"# GovOPlaN Platform Interface Inventory",
|
"# GovOPlaN Platform Interface Inventory",
|
||||||
"",
|
"",
|
||||||
@@ -900,6 +1038,12 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
|
|||||||
f"- Fields with a resolvable F1 context: {summary['ui_fields_with_resolvable_f1_context']}",
|
f"- Fields with a resolvable F1 context: {summary['ui_fields_with_resolvable_f1_context']}",
|
||||||
f"- Fields with dynamic help references: {summary['dynamic_help_references']}",
|
f"- Fields with dynamic help references: {summary['dynamic_help_references']}",
|
||||||
f"- Help review candidates: {summary['help_review_candidates']}",
|
f"- Help review candidates: {summary['help_review_candidates']}",
|
||||||
|
f"- High-risk controls: {summary['high_risk_controls']}",
|
||||||
|
f"- High-risk controls with exact F1 help: {summary['high_risk_controls_with_exact_help']}",
|
||||||
|
f"- High-risk controls missing exact F1 help: {summary['high_risk_controls_missing_exact_help']}",
|
||||||
|
f"- Invalid help-risk annotations: {summary['invalid_help_risk_annotations']}",
|
||||||
|
f"- High-risk exact contexts missing a manifest topic: {summary['unresolved_exact_high_risk_help']}",
|
||||||
|
f"- High-risk contexts without complete German topic content: {summary['high_risk_help_without_german']}",
|
||||||
f"- Stable interface declarations: {summary['interface_declarations']}",
|
f"- Stable interface declarations: {summary['interface_declarations']}",
|
||||||
f"- Duplicate interface IDs: {summary['duplicate_interface_ids']}",
|
f"- Duplicate interface IDs: {summary['duplicate_interface_ids']}",
|
||||||
f"- WebUI surfaces missing runtime declarations: {summary['undeclared_source_surfaces']}",
|
f"- WebUI surfaces missing runtime declarations: {summary['undeclared_source_surfaces']}",
|
||||||
@@ -930,6 +1074,23 @@ def _render_markdown(inventory: dict[str, Any]) -> str:
|
|||||||
f"| `{repository}` | {count} |"
|
f"| `{repository}` | {count} |"
|
||||||
for repository, count in sorted(help_by_repository.items())
|
for repository, count in sorted(help_by_repository.items())
|
||||||
)
|
)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## High-risk Contextual-help Debt",
|
||||||
|
"",
|
||||||
|
"Inferred or explicitly classified high-risk controls require an exact",
|
||||||
|
"F1 context. `data-help-risk-reviewed=\"standard\"` records a reviewed",
|
||||||
|
"false positive. The versioned baseline makes this queue non-regressing.",
|
||||||
|
"",
|
||||||
|
"| Repository | Missing exact contexts |",
|
||||||
|
"| --- | ---: |",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
lines.extend(
|
||||||
|
f"| `{repository}` | {count} |"
|
||||||
|
for repository, count in sorted(high_risk_by_repository.items())
|
||||||
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
@@ -1005,6 +1166,29 @@ def endpoint_key(endpoint: dict[str, Any]) -> tuple[str, str, str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_high_risk_help_baseline(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"High-risk contextual-help baseline does not exist: {path}"
|
||||||
|
) from exc
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"High-risk contextual-help baseline is invalid JSON: {exc}"
|
||||||
|
) from exc
|
||||||
|
if not isinstance(payload, dict) or payload.get("schema_version") != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"High-risk contextual-help baseline must use schema_version 1."
|
||||||
|
)
|
||||||
|
maximum = payload.get("maximum_missing_exact_help")
|
||||||
|
if not isinstance(maximum, int) or isinstance(maximum, bool) or maximum < 0:
|
||||||
|
raise ValueError(
|
||||||
|
"High-risk contextual-help baseline maximum must be a non-negative integer."
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def _load_endpoint_declarations(
|
def _load_endpoint_declarations(
|
||||||
path: Path,
|
path: Path,
|
||||||
) -> dict[tuple[str, str, str], dict[str, Any]]:
|
) -> dict[tuple[str, str, str], dict[str, Any]]:
|
||||||
|
|||||||
Reference in New Issue
Block a user