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.
API Shape
Section titled “API Shape”Project management routes use the internal numeric project ID in the path.
Project indicator templates:
POST /api/v2/projects/:projectId/indicatorsGET /api/v2/projects/:projectId/indicatorsGET /api/v2/projects/:projectId/indicators/:indicatorIdDELETE /api/v2/projects/:projectId/indicators/:indicatorId
Project indicator applications:
POST /api/v2/projects/:projectId/indicators/:indicatorId/applicationsGET /api/v2/projects/:projectId/indicators/:indicatorId/applicationsGET /api/v2/projects/:projectId/indicator-applicationsGET /api/v2/projects/:projectId/indicator-applications/:applicationIdPOST /api/v2/projects/:projectId/indicator-applications/:applicationId/enablePOST /api/v2/projects/:projectId/indicator-applications/:applicationId/disableDELETE /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.
Template Fields
Section titled “Template Fields”Project indicators are custom indicators with scope = "project" and a
non-null project_id. They require:
triggerTypeextractionRulescriteriadeclarationProfile.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.
Legacy Backfill
Section titled “Legacy Backfill”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.
Event Routing
Section titled “Event Routing”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.
Timeline Evidence Publishing
Section titled “Timeline Evidence Publishing”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:
passpublishes assuccesspendingpublishes aspendingfailanderrorpublish asfailed