create-principles-disciple 1.114.0 → 1.115.0

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.
Files changed (43) hide show
  1. package/console/dist/server/index.js +8 -0
  2. package/console/dist/server/models/GovernanceProjectionCollector.d.ts +16 -0
  3. package/console/dist/server/models/GovernanceProjectionCollector.js +540 -0
  4. package/console/dist/server/routes/principles.d.ts +5 -1
  5. package/console/dist/server/routes/principles.js +44 -1
  6. package/console/dist/ui/api.d.ts +3 -1
  7. package/console/dist/ui/api.js +5 -2
  8. package/console/dist/ui/i18n/en.json +82 -1
  9. package/console/dist/ui/i18n/zh-CN.json +82 -1
  10. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +26 -5
  11. package/console/dist/ui/utils/validators.d.ts +2 -0
  12. package/console/dist/ui/utils/validators.js +82 -10
  13. package/console/dist/web/assets/app.css +16 -0
  14. package/console/dist/web/assets/app.js +304 -6
  15. package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.d.ts +2 -0
  16. package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.d.ts.map +1 -0
  17. package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.js +78 -0
  18. package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.js.map +1 -0
  19. package/core/dist/runtime-v2/__tests__/governance-projection.test.d.ts +2 -0
  20. package/core/dist/runtime-v2/__tests__/governance-projection.test.d.ts.map +1 -0
  21. package/core/dist/runtime-v2/__tests__/governance-projection.test.js +154 -0
  22. package/core/dist/runtime-v2/__tests__/governance-projection.test.js.map +1 -0
  23. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +15 -0
  24. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  25. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  26. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -0
  27. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  28. package/core/dist/runtime-v2/governance-projection-contract.d.ts +721 -0
  29. package/core/dist/runtime-v2/governance-projection-contract.d.ts.map +1 -0
  30. package/core/dist/runtime-v2/governance-projection-contract.js +136 -0
  31. package/core/dist/runtime-v2/governance-projection-contract.js.map +1 -0
  32. package/core/dist/runtime-v2/governance-projection.d.ts +4 -0
  33. package/core/dist/runtime-v2/governance-projection.d.ts.map +1 -0
  34. package/core/dist/runtime-v2/governance-projection.js +176 -0
  35. package/core/dist/runtime-v2/governance-projection.js.map +1 -0
  36. package/core/dist/runtime-v2/index.d.ts +3 -0
  37. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  38. package/core/dist/runtime-v2/index.js +2 -0
  39. package/core/dist/runtime-v2/index.js.map +1 -1
  40. package/package.json +2 -2
  41. package/plugin/dist/bundle.js +471 -471
  42. package/plugin/openclaw.plugin.json +1 -1
  43. package/plugin/package.json +1 -1
@@ -209,9 +209,11 @@ async function initServices(workspaceDir, authConfig) {
209
209
  const pdFlags = computeFlagsFromLoadResult(configResult);
210
210
  const feedbackChannelEnabled = pdFlags.flags.feedback_channel?.enabled ?? false;
211
211
  const failedTasksObservabilityEnabled = pdFlags.flags.failed_tasks_observability?.enabled ?? true;
212
+ const governanceProjectionEnabled = pdFlags.flags.principle_governance_projection_v2?.enabled ?? false;
212
213
  const feedbackFlags = {
213
214
  feedback_channel: { enabled: feedbackChannelEnabled },
214
215
  failed_tasks_observability: { enabled: failedTasksObservabilityEnabled },
216
+ principle_governance_projection_v2: { enabled: governanceProjectionEnabled },
215
217
  };
216
218
  if (!configResult.ok) {
217
219
  console.warn('[pd-console] PD config loading failed (using defaults for feedback channel):', configResult.errors.map(e => e.reason).join('; '));
@@ -322,6 +324,12 @@ function handleRequest(services) {
322
324
  asyncHandler(() => handlePrinciplesRoute({ req, res, workspaceDir: services.workspaceDir, subPath }))(req, res);
323
325
  return;
324
326
  }
327
+ // GET /api/v1/principles/:id/governance
328
+ if (urlPath.startsWith('/api/v1/principles/')) {
329
+ const subPath = urlPath.slice('/api/v1/principles'.length);
330
+ asyncHandler(() => handlePrinciplesRoute({ req, res, workspaceDir: services.workspaceDir, subPath, featureFlags: services.feedbackFlags }))(req, res);
331
+ return;
332
+ }
325
333
  // Workspace management routes
326
334
  if (urlPath === '/api/workspaces' || urlPath.startsWith('/api/workspaces/')) {
327
335
  const subPath = urlPath.slice('/api/workspaces'.length);
@@ -0,0 +1,16 @@
1
+ import type { GovernanceFacts } from '@principles/core/runtime-v2';
2
+ export declare class GovernanceProjectionCollectionError extends Error {
3
+ readonly reasonCode: 'principle_not_found' | 'governance_projection_error';
4
+ readonly nextActionCode: string;
5
+ constructor(reasonCode: 'principle_not_found' | 'governance_projection_error', nextActionCode: string);
6
+ }
7
+ export declare class GovernanceProjectionCollector {
8
+ private readonly workspaceDir;
9
+ constructor(workspaceDir: string);
10
+ collect(principleId: string, asOf: string): Promise<GovernanceFacts>;
11
+ private readPrinciple;
12
+ private static parseStringArrayJson;
13
+ private static parseTaskRow;
14
+ private static connectedTaskIds;
15
+ private static finish;
16
+ }
@@ -0,0 +1,540 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { Value } from '@sinclair/typebox/value';
4
+ import { GovernanceFactsSchema, SqliteConnection, isPDErrorCategory, parsePITaskMetadata, } from '@principles/core/runtime-v2';
5
+ const GOVERNANCE_TASK_KINDS = new Set([
6
+ 'dreamer', 'philosopher', 'scribe', 'artificer', 'evaluator', 'rollout_reviewer',
7
+ ]);
8
+ const GOVERNANCE_TASK_STATUSES = new Set([
9
+ 'pending', 'leased', 'succeeded', 'retry_wait', 'failed', 'needs_human_review',
10
+ ]);
11
+ const PRINCIPLE_STATES = new Set(['candidate', 'active', 'archived', 'deprecated', 'probation']);
12
+ const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
13
+ const MAX_LINEAGE_NODES = 500;
14
+ function isRecord(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ function readOwnString(record, key) {
18
+ if (!Object.hasOwn(record, key))
19
+ return undefined;
20
+ const value = record[key];
21
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
22
+ }
23
+ function isTimestamp(value) {
24
+ if (value === undefined || !ISO_UTC.test(value))
25
+ return false;
26
+ const parsed = new Date(value);
27
+ if (!Number.isFinite(parsed.getTime()))
28
+ return false;
29
+ return parsed.toISOString() === (value.includes('.') ? value : value.replace('Z', '.000Z'));
30
+ }
31
+ function isGovernanceChannel(value) {
32
+ return value === 'prompt' || value === 'code_tool_hook' || value === 'defer_archive';
33
+ }
34
+ function issue(value) {
35
+ return value;
36
+ }
37
+ export class GovernanceProjectionCollectionError extends Error {
38
+ reasonCode;
39
+ nextActionCode;
40
+ constructor(reasonCode, nextActionCode) {
41
+ super(reasonCode);
42
+ this.reasonCode = reasonCode;
43
+ this.nextActionCode = nextActionCode;
44
+ this.name = 'GovernanceProjectionCollectionError';
45
+ }
46
+ }
47
+ export class GovernanceProjectionCollector {
48
+ workspaceDir;
49
+ constructor(workspaceDir) {
50
+ this.workspaceDir = workspaceDir;
51
+ }
52
+ async collect(principleId, asOf) {
53
+ if (principleId.length === 0 || !isTimestamp(asOf)) {
54
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'check_request_parameters');
55
+ }
56
+ const collectionIssues = [];
57
+ const principle = this.readPrinciple(principleId, collectionIssues);
58
+ const dbPath = path.join(this.workspaceDir, '.pd', 'state.db');
59
+ if (!fs.existsSync(dbPath)) {
60
+ collectionIssues.push(issue({ source: 'lineage', reasonCode: 'source_unavailable', nextActionCode: 'initialize_runtime_state' }));
61
+ return GovernanceProjectionCollector.finish({ principleId, asOf, principle, collectionIssues });
62
+ }
63
+ const connection = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
64
+ try {
65
+ const db = connection.getDb();
66
+ const artifactIds = [];
67
+ const rootTaskIds = [];
68
+ const sourceRefs = [{ type: 'principle', id: principleId }];
69
+ const artifactRows = db.prepare(`
70
+ SELECT artifact_id, source_task_id, source_principle_id, lineage_artifact_ids, updated_at
71
+ FROM pi_artifacts ORDER BY artifact_id ASC
72
+ `).all();
73
+ const validArtifacts = new Map();
74
+ for (const row of artifactRows) {
75
+ if (!isRecord(row)) {
76
+ collectionIssues.push(issue({ source: 'artifact', reasonCode: 'metadata_malformed', nextActionCode: 'repair_artifact_metadata' }));
77
+ continue;
78
+ }
79
+ const artifactId = readOwnString(row, 'artifact_id');
80
+ const sourceTaskId = readOwnString(row, 'source_task_id');
81
+ const sourcePrincipleId = readOwnString(row, 'source_principle_id');
82
+ const updatedAt = readOwnString(row, 'updated_at');
83
+ const lineageJson = readOwnString(row, 'lineage_artifact_ids');
84
+ const lineageArtifactIds = GovernanceProjectionCollector.parseStringArrayJson(lineageJson);
85
+ const artifactRef = artifactId === undefined ? undefined : { type: 'artifact', id: artifactId };
86
+ if (artifactId === undefined || sourceTaskId === undefined || !isTimestamp(updatedAt)
87
+ || lineageArtifactIds === null) {
88
+ if (sourcePrincipleId === principleId) {
89
+ collectionIssues.push(issue(artifactRef === undefined
90
+ ? { source: 'artifact', reasonCode: 'metadata_malformed', nextActionCode: 'repair_artifact_metadata' }
91
+ : { source: 'artifact', reasonCode: 'metadata_malformed', nextActionCode: 'repair_artifact_metadata', sourceRef: artifactRef }));
92
+ }
93
+ continue;
94
+ }
95
+ validArtifacts.set(artifactId, {
96
+ artifactId, sourceTaskId, lineageArtifactIds,
97
+ ...(sourcePrincipleId === undefined ? {} : { sourcePrincipleId }),
98
+ });
99
+ }
100
+ const strongArtifactIds = new Set([...validArtifacts.values()].filter(row => row.sourcePrincipleId === principleId).map(row => row.artifactId));
101
+ let addedArtifact = true;
102
+ while (addedArtifact) {
103
+ addedArtifact = false;
104
+ for (const row of validArtifacts.values()) {
105
+ if (!strongArtifactIds.has(row.artifactId) && row.lineageArtifactIds.some(id => strongArtifactIds.has(id))) {
106
+ strongArtifactIds.add(row.artifactId);
107
+ addedArtifact = true;
108
+ }
109
+ }
110
+ }
111
+ for (const artifactId of [...strongArtifactIds].sort()) {
112
+ const artifact = validArtifacts.get(artifactId);
113
+ if (artifact === undefined)
114
+ continue;
115
+ artifactIds.push(artifactId);
116
+ rootTaskIds.push(artifact.sourceTaskId);
117
+ sourceRefs.push({ type: 'artifact', id: artifactId });
118
+ }
119
+ if (artifactIds.length === 0) {
120
+ collectionIssues.push(issue({ source: 'lineage', reasonCode: 'lineage_not_available', nextActionCode: 'wait_for_durable_lineage' }));
121
+ return GovernanceProjectionCollector.finish({ principleId, asOf, principle, collectionIssues });
122
+ }
123
+ const validTasks = new Map();
124
+ const taskRowIssues = [];
125
+ const taskRows = db.prepare('SELECT * FROM tasks ORDER BY task_id ASC').all();
126
+ for (const row of taskRows) {
127
+ const parsed = GovernanceProjectionCollector.parseTaskRow(row, taskRowIssues);
128
+ if (parsed !== null)
129
+ validTasks.set(parsed.taskId, parsed);
130
+ }
131
+ const connected = GovernanceProjectionCollector.connectedTaskIds(rootTaskIds, validTasks, collectionIssues);
132
+ const taskLineageConfidence = collectionIssues.some(item => item.reasonCode === 'lineage_cycle' || item.reasonCode === 'lineage_limit_exceeded') ? 'weak' : 'strong';
133
+ for (const taskIssue of taskRowIssues) {
134
+ if (taskIssue.sourceRef?.type === 'task' && connected.has(taskIssue.sourceRef.id)) {
135
+ collectionIssues.push(taskIssue);
136
+ }
137
+ }
138
+ const tasks = [];
139
+ const runnerVerdicts = [];
140
+ const derivedRelations = [];
141
+ const revisionIdentities = [];
142
+ const timelineEvents = [];
143
+ const materializedRevisionSources = new Set();
144
+ for (const taskId of connected) {
145
+ const task = validTasks.get(taskId);
146
+ if (task === undefined) {
147
+ collectionIssues.push(issue({ source: 'task', reasonCode: 'metadata_malformed', nextActionCode: 'repair_task_metadata', sourceRef: { type: 'task', id: taskId } }));
148
+ continue;
149
+ }
150
+ if (task.revisionIdentity !== undefined) {
151
+ const identity = task.revisionIdentity;
152
+ const sourceTaskId = identity.kind === 'evaluator_repair'
153
+ ? identity.sourceEvaluatorTaskId
154
+ : identity.kind === 'rollout_reopen' ? identity.sourceRolloutTaskId : undefined;
155
+ const sourceArtifactId = identity.kind === 'evaluator_repair'
156
+ ? identity.sourceArtificerArtifactId
157
+ : identity.kind === 'rollout_reopen' ? identity.sourceArtifactId : undefined;
158
+ if (sourceTaskId === undefined || sourceArtifactId === undefined
159
+ || !connected.has(sourceTaskId) || !strongArtifactIds.has(sourceArtifactId)) {
160
+ collectionIssues.push(issue({
161
+ source: 'lineage', reasonCode: 'lineage_conflict', nextActionCode: 'repair_revision_lineage',
162
+ sourceRef: { type: 'task', id: task.taskId },
163
+ }));
164
+ continue;
165
+ }
166
+ revisionIdentities.push(identity);
167
+ materializedRevisionSources.add(sourceTaskId);
168
+ derivedRelations.push({
169
+ schemaVersion: '1', family: 'derived_relation', sourceRef: { type: 'task', id: task.taskId }, principleId,
170
+ taskId: task.taskId, lineageConfidence: taskLineageConfidence, recordedAt: task.updatedAt,
171
+ revisionIdentity: identity, relation: 'revision_materialized',
172
+ evidenceRefs: [
173
+ { type: 'task', id: sourceTaskId },
174
+ { type: 'artifact', id: sourceArtifactId },
175
+ { type: 'task', id: task.taskId },
176
+ ],
177
+ });
178
+ timelineEvents.push({
179
+ code: 'revision_reopened', occurredAt: task.createdAt, recordedAt: task.updatedAt,
180
+ summaryCode: 'governance.timeline.revision_reopened', sourceRef: { type: 'task', id: task.taskId },
181
+ lineageConfidence: taskLineageConfidence,
182
+ });
183
+ }
184
+ const fact = {
185
+ schemaVersion: '1', family: 'task', sourceRef: { type: 'task', id: task.taskId }, principleId,
186
+ taskId: task.taskId, lineageConfidence: taskLineageConfidence, recordedAt: task.updatedAt,
187
+ occurredAt: task.createdAt, taskKind: task.taskKind, channel: task.channel, status: task.status,
188
+ attemptCount: task.attemptCount, maxAttempts: task.maxAttempts,
189
+ };
190
+ if (task.leaseExpiresAt !== undefined)
191
+ fact.leaseExpiresAt = task.leaseExpiresAt;
192
+ if (task.lastErrorCategory !== undefined)
193
+ fact.lastErrorCategory = task.lastErrorCategory;
194
+ if (task.revisionIdentity !== undefined) {
195
+ fact.revisionIdentity = task.revisionIdentity;
196
+ }
197
+ if (task.completionIntent !== undefined)
198
+ fact.completionIntent = task.completionIntent;
199
+ tasks.push(fact);
200
+ sourceRefs.push(fact.sourceRef);
201
+ if ((task.taskKind === 'evaluator' || task.taskKind === 'rollout_reviewer') && task.runnerDecision !== undefined) {
202
+ runnerVerdicts.push({
203
+ schemaVersion: '1', family: 'runner_verdict', sourceRef: fact.sourceRef, principleId,
204
+ taskId: task.taskId, lineageConfidence: taskLineageConfidence, recordedAt: task.updatedAt,
205
+ runnerKind: task.taskKind, outcome: task.runnerDecision,
206
+ });
207
+ timelineEvents.push({
208
+ code: 'review_started', occurredAt: task.createdAt, recordedAt: task.updatedAt,
209
+ summaryCode: 'governance.timeline.review_started', sourceRef: fact.sourceRef, lineageConfidence: taskLineageConfidence,
210
+ });
211
+ if (task.runnerDecision === 'needs_revision') {
212
+ timelineEvents.push({
213
+ code: 'revision_requested', occurredAt: task.updatedAt, recordedAt: task.updatedAt,
214
+ summaryCode: 'governance.timeline.revision_requested', sourceRef: fact.sourceRef, lineageConfidence: taskLineageConfidence,
215
+ });
216
+ }
217
+ }
218
+ else if ((task.taskKind === 'evaluator' || task.taskKind === 'rollout_reviewer') && task.status === 'succeeded') {
219
+ derivedRelations.push({
220
+ schemaVersion: '1', family: 'derived_relation', sourceRef: fact.sourceRef, principleId,
221
+ taskId: task.taskId, lineageConfidence: taskLineageConfidence, recordedAt: task.updatedAt,
222
+ relation: 'verdict_missing', evidenceRefs: [fact.sourceRef],
223
+ });
224
+ }
225
+ if (task.status === 'failed' || task.status === 'needs_human_review') {
226
+ const code = task.status === 'failed' ? 'failed' : 'human_review';
227
+ timelineEvents.push({
228
+ code, occurredAt: task.updatedAt, recordedAt: task.updatedAt,
229
+ summaryCode: `governance.timeline.${code}`, sourceRef: fact.sourceRef, lineageConfidence: taskLineageConfidence,
230
+ });
231
+ }
232
+ }
233
+ tasks.sort((left, right) => (left.taskId ?? '').localeCompare(right.taskId ?? ''));
234
+ const strongTaskIds = new Set(tasks.filter(task => task.lineageConfidence === 'strong').map(task => task.taskId).filter((taskId) => taskId !== undefined));
235
+ for (const task of tasks) {
236
+ const { taskId } = task;
237
+ if (taskId === undefined || task.completionIntent?.status !== 'pending' || materializedRevisionSources.has(taskId))
238
+ continue;
239
+ derivedRelations.push({
240
+ schemaVersion: '1', family: 'derived_relation', sourceRef: task.sourceRef, principleId,
241
+ taskId, lineageConfidence: 'strong', recordedAt: task.recordedAt,
242
+ relation: 'revision_pending', evidenceRefs: [task.sourceRef],
243
+ });
244
+ }
245
+ for (const successor of [...validTasks.values()].sort((left, right) => left.taskId.localeCompare(right.taskId))) {
246
+ if (!strongTaskIds.has(successor.taskId))
247
+ continue;
248
+ for (const dependencyTaskId of [...successor.dependencyTaskIds].sort()) {
249
+ if (!strongTaskIds.has(dependencyTaskId))
250
+ continue;
251
+ derivedRelations.push({
252
+ schemaVersion: '1', family: 'derived_relation', sourceRef: { type: 'task', id: dependencyTaskId }, principleId,
253
+ taskId: dependencyTaskId, lineageConfidence: 'strong', recordedAt: successor.updatedAt,
254
+ relation: 'successor_present',
255
+ evidenceRefs: [{ type: 'task', id: dependencyTaskId }, { type: 'task', id: successor.taskId }],
256
+ });
257
+ }
258
+ }
259
+ const approvals = [];
260
+ const activations = [];
261
+ for (const row of db.prepare('SELECT * FROM approvals ORDER BY approval_id ASC').all()) {
262
+ if (!isRecord(row))
263
+ continue;
264
+ const artifactId = readOwnString(row, 'artifact_id');
265
+ if (artifactId === undefined || !strongArtifactIds.has(artifactId))
266
+ continue;
267
+ const approvalId = readOwnString(row, 'approval_id');
268
+ const channel = readOwnString(row, 'channel');
269
+ const outcome = readOwnString(row, 'status');
270
+ const requestedAt = readOwnString(row, 'requested_at');
271
+ const decidedAt = readOwnString(row, 'decided_at');
272
+ const approvalRef = approvalId === undefined ? undefined : { type: 'approval', id: approvalId };
273
+ if (approvalId === undefined || !isGovernanceChannel(channel) || !isTimestamp(requestedAt)
274
+ || (outcome !== 'pending' && outcome !== 'approved' && outcome !== 'rejected' && outcome !== 'cancelled')
275
+ || (decidedAt !== undefined && !isTimestamp(decidedAt))) {
276
+ collectionIssues.push(issue(approvalRef === undefined
277
+ ? { source: 'approval', reasonCode: 'metadata_malformed', nextActionCode: 'repair_approval_record' }
278
+ : { source: 'approval', reasonCode: 'metadata_malformed', nextActionCode: 'repair_approval_record', sourceRef: approvalRef }));
279
+ continue;
280
+ }
281
+ const strongApprovalRef = { type: 'approval', id: approvalId };
282
+ const fact = {
283
+ schemaVersion: '1', family: 'approval', sourceRef: strongApprovalRef, principleId, artifactId,
284
+ approvalId, channel, outcome, lineageConfidence: 'strong', recordedAt: decidedAt ?? requestedAt,
285
+ };
286
+ if (decidedAt !== undefined)
287
+ fact.occurredAt = decidedAt;
288
+ approvals.push(fact);
289
+ sourceRefs.push(strongApprovalRef);
290
+ if (outcome === 'approved' || outcome === 'rejected') {
291
+ timelineEvents.push({
292
+ code: outcome, occurredAt: decidedAt ?? requestedAt, recordedAt: decidedAt ?? requestedAt,
293
+ summaryCode: `governance.timeline.${outcome}`, sourceRef: strongApprovalRef, lineageConfidence: 'strong',
294
+ });
295
+ }
296
+ }
297
+ for (const row of db.prepare('SELECT * FROM activations ORDER BY activated_at ASC, activation_id ASC').all()) {
298
+ if (!isRecord(row))
299
+ continue;
300
+ const artifactId = readOwnString(row, 'artifact_id');
301
+ if (artifactId === undefined || !strongArtifactIds.has(artifactId))
302
+ continue;
303
+ const activationId = readOwnString(row, 'activation_id');
304
+ const channel = readOwnString(row, 'channel');
305
+ const activatedAt = readOwnString(row, 'activated_at');
306
+ const deactivatedAt = readOwnString(row, 'deactivated_at');
307
+ const activationRef = activationId === undefined ? undefined : { type: 'activation', id: activationId };
308
+ if (activationId === undefined || !isGovernanceChannel(channel) || !isTimestamp(activatedAt)
309
+ || (deactivatedAt !== undefined && !isTimestamp(deactivatedAt))) {
310
+ collectionIssues.push(issue(activationRef === undefined
311
+ ? { source: 'activation', reasonCode: 'metadata_malformed', nextActionCode: 'repair_activation_record' }
312
+ : { source: 'activation', reasonCode: 'metadata_malformed', nextActionCode: 'repair_activation_record', sourceRef: activationRef }));
313
+ continue;
314
+ }
315
+ const strongActivationRef = { type: 'activation', id: activationId };
316
+ const fact = {
317
+ schemaVersion: '1', family: 'activation', sourceRef: strongActivationRef, principleId, artifactId,
318
+ activationId, channel, outcome: deactivatedAt === undefined ? 'active' : 'deactivated',
319
+ activatedAt, lineageConfidence: 'strong', recordedAt: deactivatedAt ?? activatedAt,
320
+ };
321
+ if (deactivatedAt !== undefined)
322
+ fact.deactivatedAt = deactivatedAt;
323
+ activations.push(fact);
324
+ sourceRefs.push(strongActivationRef);
325
+ timelineEvents.push({ code: 'activated', occurredAt: activatedAt, recordedAt: activatedAt, summaryCode: 'governance.timeline.activated', sourceRef: strongActivationRef, lineageConfidence: 'strong' });
326
+ if (deactivatedAt !== undefined) {
327
+ timelineEvents.push({ code: 'deactivated', occurredAt: deactivatedAt, recordedAt: deactivatedAt, summaryCode: 'governance.timeline.deactivated', sourceRef: strongActivationRef, lineageConfidence: 'strong' });
328
+ }
329
+ }
330
+ timelineEvents.sort((left, right) => {
331
+ const timeOrder = (left.occurredAt ?? left.recordedAt).localeCompare(right.occurredAt ?? right.recordedAt);
332
+ if (timeOrder !== 0)
333
+ return timeOrder;
334
+ const typeOrder = left.sourceRef.type.localeCompare(right.sourceRef.type);
335
+ if (typeOrder !== 0)
336
+ return typeOrder;
337
+ const idOrder = left.sourceRef.id.localeCompare(right.sourceRef.id);
338
+ return idOrder !== 0 ? idOrder : left.code.localeCompare(right.code);
339
+ });
340
+ return GovernanceProjectionCollector.finish({
341
+ principleId, asOf, principle, collectionIssues, artifactIds,
342
+ taskIds: tasks.map(task => task.taskId).filter((taskId) => taskId !== undefined).sort(), tasks, revisionIdentities, sourceRefs,
343
+ runnerVerdicts, derivedRelations, approvals, activations, timelineEvents,
344
+ });
345
+ }
346
+ catch (error) {
347
+ if (error instanceof GovernanceProjectionCollectionError)
348
+ throw error;
349
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'inspect_runtime_state');
350
+ }
351
+ finally {
352
+ connection.close();
353
+ }
354
+ }
355
+ readPrinciple(principleId, issues) {
356
+ const ledgerPath = path.join(this.workspaceDir, '.state', 'principle_training_state.json');
357
+ let parsed;
358
+ try {
359
+ parsed = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
360
+ }
361
+ catch {
362
+ throw new GovernanceProjectionCollectionError('principle_not_found', 'check_principle_ledger');
363
+ }
364
+ if (!isRecord(parsed))
365
+ throw new GovernanceProjectionCollectionError('principle_not_found', 'check_principle_ledger');
366
+ const tree = Object.hasOwn(parsed, '_tree') ? parsed._tree : parsed.tree;
367
+ if (!isRecord(tree) || !isRecord(tree.principles) || !Object.hasOwn(tree.principles, principleId)) {
368
+ throw new GovernanceProjectionCollectionError('principle_not_found', 'check_principle_id');
369
+ }
370
+ const raw = tree.principles[principleId];
371
+ if (!isRecord(raw))
372
+ throw new GovernanceProjectionCollectionError('principle_not_found', 'repair_principle_ledger');
373
+ const state = readOwnString(raw, 'status');
374
+ const updatedAt = readOwnString(raw, 'updatedAt');
375
+ const createdAt = readOwnString(raw, 'createdAt');
376
+ if (state === undefined || !PRINCIPLE_STATES.has(state)) {
377
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'repair_principle_ledger');
378
+ }
379
+ const recordedAt = isTimestamp(updatedAt) ? updatedAt : createdAt;
380
+ if (!isTimestamp(recordedAt)) {
381
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'repair_principle_timestamp');
382
+ }
383
+ if (!isTimestamp(updatedAt)) {
384
+ issues.push(issue({ source: 'ledger', reasonCode: 'metadata_malformed', nextActionCode: 'repair_principle_updated_at', sourceRef: { type: 'principle', id: principleId } }));
385
+ }
386
+ if (state !== 'candidate' && state !== 'active' && state !== 'archived' && state !== 'deprecated' && state !== 'probation') {
387
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'repair_principle_status');
388
+ }
389
+ return { schemaVersion: '1', family: 'principle', sourceRef: { type: 'principle', id: principleId }, principleId, lineageConfidence: 'strong', recordedAt, state };
390
+ }
391
+ static parseStringArrayJson(value) {
392
+ if (value === undefined)
393
+ return null;
394
+ try {
395
+ const parsed = JSON.parse(value);
396
+ return Array.isArray(parsed) && parsed.every(item => typeof item === 'string' && item.length > 0) ? parsed : null;
397
+ }
398
+ catch {
399
+ return null;
400
+ }
401
+ }
402
+ static parseTaskRow(row, issues) {
403
+ if (!isRecord(row)) {
404
+ issues.push(issue({ source: 'task', reasonCode: 'metadata_malformed', nextActionCode: 'repair_task_metadata' }));
405
+ return null;
406
+ }
407
+ const taskId = readOwnString(row, 'task_id');
408
+ const taskRef = taskId === undefined ? undefined : { type: 'task', id: taskId };
409
+ const taskKind = readOwnString(row, 'task_kind');
410
+ const status = readOwnString(row, 'status');
411
+ const createdAt = readOwnString(row, 'created_at');
412
+ const updatedAt = readOwnString(row, 'updated_at');
413
+ const diagnosticJson = readOwnString(row, 'diagnostic_json');
414
+ const attemptCount = Object.hasOwn(row, 'attempt_count') ? row.attempt_count : undefined;
415
+ const maxAttempts = Object.hasOwn(row, 'max_attempts') ? row.max_attempts : undefined;
416
+ const metadata = diagnosticJson === undefined ? null : parsePITaskMetadata(diagnosticJson);
417
+ const channel = metadata?.channel;
418
+ if (taskId === undefined || taskKind === undefined || !GOVERNANCE_TASK_KINDS.has(taskKind)
419
+ || status === undefined || !GOVERNANCE_TASK_STATUSES.has(status) || !isTimestamp(createdAt)
420
+ || !isTimestamp(updatedAt) || !Number.isInteger(attemptCount) || !Number.isInteger(maxAttempts)
421
+ || typeof attemptCount !== 'number' || attemptCount < 0 || typeof maxAttempts !== 'number' || maxAttempts < 1
422
+ || metadata === null
423
+ || (channel !== 'prompt' && channel !== 'code_tool_hook' && channel !== 'defer_archive')) {
424
+ issues.push(issue(taskRef === undefined
425
+ ? { source: 'task', reasonCode: 'metadata_malformed', nextActionCode: 'repair_task_metadata' }
426
+ : { source: 'task', reasonCode: 'metadata_malformed', nextActionCode: 'repair_task_metadata', sourceRef: taskRef }));
427
+ return null;
428
+ }
429
+ if (taskKind !== 'dreamer' && taskKind !== 'philosopher' && taskKind !== 'scribe' && taskKind !== 'artificer' && taskKind !== 'evaluator' && taskKind !== 'rollout_reviewer')
430
+ return null;
431
+ if (status !== 'pending' && status !== 'leased' && status !== 'succeeded' && status !== 'retry_wait' && status !== 'failed' && status !== 'needs_human_review')
432
+ return null;
433
+ const result = { taskId, taskKind, status, createdAt, updatedAt, attemptCount, maxAttempts, channel, dependencyTaskIds: metadata.dependencyTaskIds };
434
+ const leaseExpiresAt = readOwnString(row, 'lease_expires_at');
435
+ if (leaseExpiresAt !== undefined) {
436
+ if (!isTimestamp(leaseExpiresAt))
437
+ issues.push(issue({ source: 'task', reasonCode: 'timestamp_invalid', nextActionCode: 'repair_task_timestamp', sourceRef: taskRef }));
438
+ else
439
+ result.leaseExpiresAt = leaseExpiresAt;
440
+ }
441
+ const lastError = readOwnString(row, 'last_error');
442
+ if (lastError !== undefined && isPDErrorCategory(lastError))
443
+ result.lastErrorCategory = lastError;
444
+ if (metadata.repairPayload !== undefined) {
445
+ result.revisionIdentity = { kind: 'evaluator_repair', sourceEvaluatorTaskId: metadata.repairPayload.sourceEvaluatorTaskId, sourceArtificerArtifactId: metadata.repairPayload.sourceArtificerArtifactId, repairIteration: metadata.repairPayload.repairIteration };
446
+ }
447
+ else if (metadata.rolloutRevisionPayload !== undefined && metadata.revisionCauseId !== undefined) {
448
+ result.revisionIdentity = { kind: 'rollout_reopen', causeId: metadata.revisionCauseId, sourceRolloutTaskId: metadata.rolloutRevisionPayload.sourceRolloutTaskId, sourceArtifactId: metadata.rolloutRevisionPayload.sourceArtifactId, revisionIteration: metadata.rolloutRevisionPayload.revisionIteration, taskRevisionEpoch: metadata.revisionCount };
449
+ }
450
+ if (metadata.completionIntent !== undefined) {
451
+ result.completionIntent = { status: metadata.completionIntent.status, revisionEpoch: metadata.completionIntent.revisionEpoch, effect: metadata.completionIntent.effect ?? 'governance_transition' };
452
+ }
453
+ if (metadata.runnerDecision !== undefined)
454
+ result.runnerDecision = metadata.runnerDecision;
455
+ return result;
456
+ }
457
+ static connectedTaskIds(rootIds, tasks, issues) {
458
+ const adjacency = new Map();
459
+ const successors = new Map();
460
+ const connect = (left, right) => {
461
+ if (left === right) {
462
+ issues.push(issue({ source: 'lineage', reasonCode: 'lineage_cycle', nextActionCode: 'repair_task_dependencies', sourceRef: { type: 'task', id: left } }));
463
+ return;
464
+ }
465
+ if (!adjacency.has(left))
466
+ adjacency.set(left, new Set());
467
+ if (!adjacency.has(right))
468
+ adjacency.set(right, new Set());
469
+ adjacency.get(left)?.add(right);
470
+ adjacency.get(right)?.add(left);
471
+ if (!successors.has(left))
472
+ successors.set(left, new Set());
473
+ successors.get(left)?.add(right);
474
+ };
475
+ for (const task of tasks.values())
476
+ for (const dependency of task.dependencyTaskIds)
477
+ connect(dependency, task.taskId);
478
+ const visited = new Set();
479
+ const queue = [...new Set(rootIds)].sort();
480
+ while (queue.length > 0) {
481
+ const current = queue.shift();
482
+ if (current === undefined || visited.has(current))
483
+ continue;
484
+ if (visited.size >= MAX_LINEAGE_NODES) {
485
+ issues.push(issue({ source: 'lineage', reasonCode: 'lineage_limit_exceeded', nextActionCode: 'reduce_or_repair_lineage' }));
486
+ break;
487
+ }
488
+ visited.add(current);
489
+ for (const neighbor of [...(adjacency.get(current) ?? [])].sort())
490
+ if (!visited.has(neighbor))
491
+ queue.push(neighbor);
492
+ }
493
+ const colors = new Map();
494
+ let cycleId;
495
+ const visit = (taskId) => {
496
+ if (colors.get(taskId) === 'visiting') {
497
+ cycleId = taskId;
498
+ return true;
499
+ }
500
+ if (colors.get(taskId) === 'visited')
501
+ return false;
502
+ colors.set(taskId, 'visiting');
503
+ for (const successor of successors.get(taskId) ?? []) {
504
+ if (visited.has(successor) && visit(successor))
505
+ return true;
506
+ }
507
+ colors.set(taskId, 'visited');
508
+ return false;
509
+ };
510
+ for (const taskId of [...visited].sort()) {
511
+ if (visit(taskId))
512
+ break;
513
+ }
514
+ if (cycleId !== undefined) {
515
+ issues.push(issue({ source: 'lineage', reasonCode: 'lineage_cycle', nextActionCode: 'repair_task_dependencies', sourceRef: { type: 'task', id: cycleId } }));
516
+ }
517
+ return visited;
518
+ }
519
+ static finish(input) {
520
+ const hasArtifacts = (input.artifactIds?.length ?? 0) > 0;
521
+ const facts = {
522
+ schemaVersion: '1', principleId: input.principleId, asOf: input.asOf,
523
+ lineage: {
524
+ principleId: input.principleId, artifactIds: input.artifactIds ?? [], taskIds: input.taskIds ?? [],
525
+ revisionIdentities: input.revisionIdentities ?? [],
526
+ confidence: hasArtifacts
527
+ ? (input.collectionIssues.some(item => item.source === 'artifact' || item.source === 'task' || item.source === 'lineage') ? 'weak' : 'strong')
528
+ : 'unknown',
529
+ sourceRefs: input.sourceRefs ?? [{ type: 'principle', id: input.principleId }],
530
+ },
531
+ principle: input.principle, tasks: input.tasks ?? [], runnerVerdicts: input.runnerVerdicts ?? [], derivedRelations: input.derivedRelations ?? [],
532
+ approvals: input.approvals ?? [], activations: input.activations ?? [],
533
+ timelineEvents: input.timelineEvents ?? [], collectionIssues: input.collectionIssues,
534
+ };
535
+ if (!Value.Check(GovernanceFactsSchema, facts)) {
536
+ throw new GovernanceProjectionCollectionError('governance_projection_error', 'inspect_projection_contract');
537
+ }
538
+ return facts;
539
+ }
540
+ }
@@ -4,7 +4,11 @@ interface PrinciplesRouteParams {
4
4
  res: ServerResponse;
5
5
  workspaceDir: string;
6
6
  subPath: string;
7
+ featureFlags?: Record<string, {
8
+ enabled: boolean;
9
+ }>;
10
+ now?: () => string;
7
11
  }
8
- export declare function handlePrinciplesRoute({ req, res, workspaceDir, subPath, }: PrinciplesRouteParams): Promise<void>;
12
+ export declare function handlePrinciplesRoute({ req, res, workspaceDir, subPath, featureFlags, now, }: PrinciplesRouteParams): Promise<void>;
9
13
  export declare function disposePrinciplesModels(): void;
10
14
  export {};
@@ -2,6 +2,9 @@ import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
  import { PrinciplesConsoleModel } from '../models/PrinciplesConsoleModel.js';
4
4
  import { PrincipleTrajectoryModel } from '../models/PrincipleTrajectoryModel.js';
5
+ import { GovernanceProjectionCollector, GovernanceProjectionCollectionError } from '../models/GovernanceProjectionCollector.js';
6
+ import { OwnerGovernanceViewSchema, deriveOwnerGovernanceView } from '@principles/core/runtime-v2';
7
+ import { Value } from '@sinclair/typebox/value';
5
8
  import { sendSuccess, sendError, sendNotFound } from '../utils/response.js';
6
9
  const models = new Map();
7
10
  const trajectoryModels = new Map();
@@ -72,7 +75,47 @@ async function getDecidedPrincipleIds(workspaceDir) {
72
75
  closeFn?.();
73
76
  }
74
77
  }
75
- export async function handlePrinciplesRoute({ req, res, workspaceDir, subPath, }) {
78
+ export async function handlePrinciplesRoute({ req, res, workspaceDir, subPath, featureFlags, now = () => new Date().toISOString(), }) {
79
+ // GET /api/v1/principles/:id/governance. Gate before constructing any
80
+ // Console model or projection reader so flag-off is a true no-read path.
81
+ const governanceMatch = /^\/([^/]+)\/governance$/.exec(subPath);
82
+ if (req.method === 'GET' && governanceMatch) {
83
+ const flag = featureFlags?.principle_governance_projection_v2;
84
+ if (flag?.enabled !== true) {
85
+ sendError(res, 403, 'feature_disabled', 'Principle governance projection is disabled.', { reason: 'feature_disabled', nextAction: 'Enable features.principle_governance_projection_v2 in .pd/config.yaml.' });
86
+ return;
87
+ }
88
+ const [, rawPrincipleId] = governanceMatch;
89
+ let principleId;
90
+ try {
91
+ principleId = decodeURIComponent(rawPrincipleId ?? '');
92
+ }
93
+ catch {
94
+ sendError(res, 400, 'invalid_principle_id', 'Principle ID contains invalid URL encoding', { nextAction: 'Check the principle ID and retry.' });
95
+ return;
96
+ }
97
+ if (principleId.length === 0) {
98
+ sendError(res, 400, 'invalid_principle_id', 'Principle ID is missing', { nextAction: 'Provide a non-empty principle ID.' });
99
+ return;
100
+ }
101
+ try {
102
+ const facts = await new GovernanceProjectionCollector(workspaceDir).collect(principleId, now());
103
+ const view = deriveOwnerGovernanceView(facts);
104
+ if (!Value.Check(OwnerGovernanceViewSchema, view)) {
105
+ sendError(res, 500, 'governance_projection_error', 'Derived governance view failed contract validation.', { nextAction: 'Inspect projection diagnostics and Runtime state.' });
106
+ return;
107
+ }
108
+ sendSuccess(res, view);
109
+ }
110
+ catch (error) {
111
+ if (error instanceof GovernanceProjectionCollectionError) {
112
+ sendError(res, error.reasonCode === 'principle_not_found' ? 404 : 500, error.reasonCode, error.message, { nextAction: error.nextActionCode });
113
+ return;
114
+ }
115
+ sendError(res, 500, 'governance_projection_error', error instanceof Error ? error.message : String(error), { nextAction: 'inspect_runtime_state' });
116
+ }
117
+ return;
118
+ }
76
119
  const model = getModel(workspaceDir);
77
120
  // ── POST Routes ─────────────────────────────────────────────────────────────
78
121
  if (req.method === 'POST') {