create-principles-disciple 1.113.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.
- package/console/dist/server/index.js +8 -0
- package/console/dist/server/models/GovernanceProjectionCollector.d.ts +16 -0
- package/console/dist/server/models/GovernanceProjectionCollector.js +540 -0
- package/console/dist/server/routes/principles.d.ts +5 -1
- package/console/dist/server/routes/principles.js +44 -1
- package/console/dist/ui/api.d.ts +3 -1
- package/console/dist/ui/api.js +5 -2
- package/console/dist/ui/i18n/en.json +82 -1
- package/console/dist/ui/i18n/zh-CN.json +82 -1
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +26 -5
- package/console/dist/ui/utils/validators.d.ts +2 -0
- package/console/dist/ui/utils/validators.js +82 -10
- package/console/dist/web/assets/app.css +16 -0
- package/console/dist/web/assets/app.js +304 -6
- package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.js +78 -0
- package/core/dist/runtime-v2/__tests__/governance-projection-contract.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/governance-projection.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/governance-projection.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/governance-projection.test.js +154 -0
- package/core/dist/runtime-v2/__tests__/governance-projection.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +15 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +9 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/governance-projection-contract.d.ts +721 -0
- package/core/dist/runtime-v2/governance-projection-contract.d.ts.map +1 -0
- package/core/dist/runtime-v2/governance-projection-contract.js +136 -0
- package/core/dist/runtime-v2/governance-projection-contract.js.map +1 -0
- package/core/dist/runtime-v2/governance-projection.d.ts +4 -0
- package/core/dist/runtime-v2/governance-projection.d.ts.map +1 -0
- package/core/dist/runtime-v2/governance-projection.js +176 -0
- package/core/dist/runtime-v2/governance-projection.js.map +1 -0
- package/core/dist/runtime-v2/index.d.ts +3 -0
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +2 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/artificer-output-retry-flag.test.d.ts +2 -0
- package/core/dist/runtime-v2/internalization/__tests__/artificer-output-retry-flag.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/__tests__/artificer-output-retry-flag.test.js +95 -0
- package/core/dist/runtime-v2/internalization/__tests__/artificer-output-retry-flag.test.js.map +1 -0
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +25 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.js +37 -3
- package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/runtime-init.d.ts +6 -0
- package/pd-cli/dist/commands/runtime-init.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-init.js +127 -2
- package/pd-cli/dist/commands/runtime-init.js.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.js +6 -1
- package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.js +6 -1
- package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/plugin/dist/bundle.js +471 -471
- package/plugin/openclaw.plugin.json +1 -1
- 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') {
|