Skip to content

Project-Scoped Indicators

GOV-80 moves custom indicators from organization-level registry entries to project-owned templates. A project indicator owns the behavior for an indicator: trigger type, parameters, extraction rules, criteria, and declaration profile. Controls now apply a project indicator through lightweight application rows that only carry lifecycle status.

Project management routes use the internal numeric project ID in the path.

Project indicator templates:

  • POST /api/v2/projects/:projectId/indicators
  • GET /api/v2/projects/:projectId/indicators
  • GET /api/v2/projects/:projectId/indicators/:indicatorId
  • DELETE /api/v2/projects/:projectId/indicators/:indicatorId

Project indicator applications:

  • POST /api/v2/projects/:projectId/indicators/:indicatorId/applications
  • GET /api/v2/projects/:projectId/indicators/:indicatorId/applications
  • GET /api/v2/projects/:projectId/indicator-applications
  • GET /api/v2/projects/:projectId/indicator-applications/:applicationId
  • POST /api/v2/projects/:projectId/indicator-applications/:applicationId/enable
  • POST /api/v2/projects/:projectId/indicator-applications/:applicationId/disable
  • DELETE /api/v2/projects/:projectId/indicator-applications/:applicationId

The legacy unscoped /api/v2/indicators and /api/v2/configurations management routes are no longer registered. Project-scoped routes are the supported management surface.

Project indicators are custom indicators with scope = "project" and a non-null project_id. They require:

  • triggerType
  • extractionRules
  • criteria
  • declarationProfile.emitOn

The same template is used for every application of the indicator. Application rows in project_policy_indicator_application point to a project policy control and the indicator UUID, and use application_status for active, disabled, or archived lifecycle state.

Migration 111_backfill_project_scoped_indicators moves valid legacy configuration behavior into project-owned indicator templates. It creates one project indicator per existing configuration, updates the configuration row in place to point at that new indicator, and preserves the configuration id and configuration_uuid.

The backfill also rewrites evaluation_results_v2.indicator_id for migrated configurations so historical evaluations appear under the project indicator while staying attached to the same configuration row. Legacy indicators that no longer have references are deleted; legacy indicators with remaining references are archived.

Rows that cannot be migrated safely are left unchanged. The migration records both migrated and skipped rows in indicator_project_backfill_audit, including the old indicator/configuration snapshots and a skip reason for invalid data.

Indicator events should route by project UUID and project indicator UUID:

{
"eventType": "indicator",
"source": "external",
"projectId": "11111111-1111-1111-1111-111111111111",
"indicatorId": "00000000-0000-0000-0000-000000000000",
"payload": {},
"timestamp": "2026-05-23T14:30:00Z"
}

The ingestion service resolves the project UUID to the internal project ID, then resolves indicatorId within that project and processes every active application for that project indicator.

configurationUuid, indicatorName, and controlId routing have been removed. Indicator events must use projectId plus indicatorId.

Automated declaration evidence for indicator evaluations uses the project and evaluation oriented timeline schema:

{
"result": {
"evaluationId": "44444444-4444-4444-8444-444444444444",
"projectId": "11111111-1111-1111-1111-111111111111",
"indicatorId": "00000000-0000-0000-0000-000000000000",
"timestamp": "2026-05-23T14:30:00Z",
"failureReasons": ["Field 'piiPresent': Expected false, got true"],
"failureDetails": [
{
"criteriaIndex": 0,
"field": "piiPresent",
"extractionPath": "$.observations[*].piiPresent",
"elementPath": "$.observations[1].piiPresent",
"indexPath": [1],
"actualValue": true,
"expectedValue": false,
"operator": "eq",
"criteriaType": "boolean",
"arrayMatchMode": "all",
"reason": "Expected false, got true"
}
],
"status": "failed"
},
"executionTime": "2026-05-23T14:30:00Z",
"evaluationCriteria": [
{
"type": "boolean",
"field": "piiPresent",
"criteriaValue": false,
"operator": "eq",
"arrayMatchMode": "all"
}
],
"payload": {
"eventName": "failure",
"observations": [{ "piiPresent": false }, { "piiPresent": true }]
},
"extracted": [
{
"field": "piiPresent",
"extractionPath": "$.observations[*].piiPresent",
"value": [false, true]
}
]
}

The published evidence does not include result.configurationId, result.overallStatus, or top-level parameters. The top-level payload matches the ingested event payload exactly, and extracted lists configured extraction fields that were present in the evaluation result.

Timeline status values are normalized as follows:

  • pass publishes as success
  • pending publishes as pending
  • fail and error publish as failed