create-principles-disciple 1.143.0 → 1.143.1

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 (60) hide show
  1. package/console/dist/server/index.js +19 -0
  2. package/console/dist/server/models/OwnerDecisionViewModel.d.ts +65 -0
  3. package/console/dist/server/models/OwnerDecisionViewModel.js +686 -0
  4. package/console/dist/server/routes/owner-decision.d.ts +40 -0
  5. package/console/dist/server/routes/owner-decision.js +88 -0
  6. package/console/dist/server/routes/principles.d.ts +3 -0
  7. package/console/dist/server/routes/principles.js +3 -0
  8. package/console/dist/ui/api.d.ts +7 -3
  9. package/console/dist/ui/api.js +13 -2
  10. package/console/dist/ui/i18n/en.json +70 -4
  11. package/console/dist/ui/i18n/zh-CN.json +70 -4
  12. package/console/dist/ui/pages/focus/FocusPage.js +25 -2
  13. package/console/dist/ui/pages/principles/PrincipleDetailPage.d.ts +24 -0
  14. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +168 -210
  15. package/console/dist/ui/pages/principles/PrinciplesPage.js +25 -10
  16. package/console/dist/ui/utils/enum-labels.d.ts +1 -1
  17. package/console/dist/ui/utils/enum-labels.js +8 -0
  18. package/console/dist/ui/utils/validators.d.ts +99 -1
  19. package/console/dist/ui/utils/validators.js +115 -0
  20. package/console/dist/web/assets/app.css +3 -0
  21. package/console/dist/web/assets/app.js +590 -349
  22. package/core/dist/runtime-v2/adapter/__tests__/principle-tree-ledger-adapter.test.js +25 -0
  23. package/core/dist/runtime-v2/adapter/__tests__/principle-tree-ledger-adapter.test.js.map +1 -1
  24. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts +16 -0
  25. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts.map +1 -1
  26. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js +22 -0
  27. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js.map +1 -1
  28. package/core/dist/runtime-v2/index.d.ts +7 -0
  29. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  30. package/core/dist/runtime-v2/index.js +4 -0
  31. package/core/dist/runtime-v2/index.js.map +1 -1
  32. package/core/dist/runtime-v2/owner-decision/__tests__/owner-decision-view.test.d.ts +2 -0
  33. package/core/dist/runtime-v2/owner-decision/__tests__/owner-decision-view.test.d.ts.map +1 -0
  34. package/core/dist/runtime-v2/owner-decision/__tests__/owner-decision-view.test.js +559 -0
  35. package/core/dist/runtime-v2/owner-decision/__tests__/owner-decision-view.test.js.map +1 -0
  36. package/core/dist/runtime-v2/owner-decision/owner-decision-view-contract.d.ts +7868 -0
  37. package/core/dist/runtime-v2/owner-decision/owner-decision-view-contract.d.ts.map +1 -0
  38. package/core/dist/runtime-v2/owner-decision/owner-decision-view-contract.js +367 -0
  39. package/core/dist/runtime-v2/owner-decision/owner-decision-view-contract.js.map +1 -0
  40. package/core/dist/runtime-v2/owner-decision/owner-decision-view.d.ts +415 -0
  41. package/core/dist/runtime-v2/owner-decision/owner-decision-view.d.ts.map +1 -0
  42. package/core/dist/runtime-v2/owner-decision/owner-decision-view.js +856 -0
  43. package/core/dist/runtime-v2/owner-decision/owner-decision-view.js.map +1 -0
  44. package/core/dist/runtime-v2/owner-decision/semantic-selector.d.ts +72 -0
  45. package/core/dist/runtime-v2/owner-decision/semantic-selector.d.ts.map +1 -0
  46. package/core/dist/runtime-v2/owner-decision/semantic-selector.js +176 -0
  47. package/core/dist/runtime-v2/owner-decision/semantic-selector.js.map +1 -0
  48. package/package.json +1 -1
  49. package/pd-cli/dist/commands/runtime-internalization-run-rulehost.d.ts +14 -0
  50. package/pd-cli/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  51. package/pd-cli/dist/commands/runtime-internalization-run-rulehost.js +47 -7
  52. package/pd-cli/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  53. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts +84 -0
  54. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  55. package/pd-cli/dist/services/rulehost-pipeline-runner.js +215 -38
  56. package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
  57. package/plugin/dist/bundle.js +526 -526
  58. package/plugin/dist/governance-audit.js +205 -205
  59. package/plugin/dist/rulehost-evidence.js +246 -246
  60. package/release-manager/package.json +1 -1
@@ -0,0 +1,686 @@
1
+ /**
2
+ * OwnerDecisionViewModel — the collecting boundary for the canonical
3
+ * Owner-facing decision projection (Owner Decision Experience v1, Phase B).
4
+ *
5
+ * Gathers the sources `deriveOwnerDecisionView` needs and stays READ-ONLY:
6
+ * - ledger entry (principle_training_state.json, via the collector's parser)
7
+ * - GovernanceFacts (GovernanceProjectionCollector — same 4-table batch read
8
+ * the existing governance endpoint uses; one snapshot per request)
9
+ * - candidate / generic diagnosis artifact / PI scribe + philosopher texts
10
+ * (diagnostic read-through axes, SPEC §9.2 — never conflated with
11
+ * governance lineage)
12
+ * - behavior application counts (principle_applications, existing ledger)
13
+ * + receipt coverage availability
14
+ *
15
+ * It writes nothing, creates nothing, and does not mutate flags. Every source
16
+ * read is recorded in `sourceReads`; failures degrade the specific source only.
17
+ */
18
+ /* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after the class, matching codebase convention */
19
+ /* eslint-disable @typescript-eslint/class-methods-use-this -- lifecycle-style methods; kept on the model for cohesion */
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { GovernanceProjectionCollector, isRecord, readOwnString, } from './GovernanceProjectionCollector.js';
23
+ import { deriveOwnerDecisionView, } from '@principles/core/runtime-v2';
24
+ import { SqliteConnection } from '@principles/core/runtime-v2';
25
+ const ISO_NOW = () => new Date().toISOString();
26
+ export class OwnerDecisionViewModel {
27
+ workspaceDir;
28
+ constructor(workspaceDir) {
29
+ this.workspaceDir = workspaceDir;
30
+ }
31
+ /** Full OwnerDecisionViewCore for one principle (Detail first layer). */
32
+ async getView(principleId, now = ISO_NOW()) {
33
+ const inputs = await this.collectInputs(principleId, now);
34
+ return deriveOwnerDecisionView(inputs);
35
+ }
36
+ /** Compact inbox projection over every ledger principle (§11.1). */
37
+ async getInbox(now = ISO_NOW()) {
38
+ const snapshot = this.readBatchSnapshot();
39
+ let ledgerParsed;
40
+ try {
41
+ ledgerParsed = this.parseLedger();
42
+ }
43
+ catch {
44
+ // No readable ledger: degrade observably instead of an empty success
45
+ // (SPEC §15 — an unavailable ledger is not "zero principles, all fine").
46
+ return {
47
+ schemaVersion: '1', generatedAt: now, totalPrinciples: 0,
48
+ groups: { decision: [], blocked: [], recovery: [] },
49
+ historicalUnknown: { count: 0, reasonText: '' },
50
+ degraded: { reason: 'principle ledger unavailable', nextAction: '检查原则账本文件。' },
51
+ };
52
+ }
53
+ const principlesTree = GovernanceProjectionCollector.principleTreeFromLedger(ledgerParsed);
54
+ if (principlesTree === null) {
55
+ return {
56
+ schemaVersion: '1', generatedAt: now, totalPrinciples: 0,
57
+ groups: { decision: [], blocked: [], recovery: [] },
58
+ historicalUnknown: { count: 0, reasonText: '' },
59
+ degraded: { reason: 'principle ledger unavailable', nextAction: '检查原则账本文件。' },
60
+ };
61
+ }
62
+ const decision = [];
63
+ const blocked = [];
64
+ const recovery = [];
65
+ let historicalUnknown = 0;
66
+ let total = 0;
67
+ for (const principleId of Object.keys(principlesTree).sort()) {
68
+ total += 1;
69
+ try {
70
+ const inputs = this.buildInputsFor({ principleId, ledgerParsed, snapshot, now, compact: true });
71
+ const view = deriveOwnerDecisionView(inputs);
72
+ const item = {
73
+ principleId,
74
+ decisionState: view.decisionState,
75
+ learnedPrinciple: view.learnedPrinciple.status === 'known'
76
+ ? { status: 'known', text: view.learnedPrinciple.value.text, sourceTier: view.learnedPrinciple.value.sourceTier }
77
+ : { status: 'unknown', reasonText: view.learnedPrinciple.reason.ownerText },
78
+ inbox: view.inbox,
79
+ blockerCodes: view.blockers.map((blocker) => blocker.reason.code),
80
+ nextAction: view.nextAction,
81
+ };
82
+ if (view.inbox.group === 'decision')
83
+ decision.push(item);
84
+ else if (view.inbox.group === 'blocked')
85
+ blocked.push(item);
86
+ else if (view.inbox.group === 'recovery')
87
+ recovery.push(item);
88
+ if (view.inbox.group === 'blocked' && view.inbox.attention === 'aggregate')
89
+ historicalUnknown += 1;
90
+ }
91
+ catch {
92
+ // A single principle failing derivation must not take down the inbox;
93
+ // it is counted as blocked-judgment so it stays observable.
94
+ blocked.push({
95
+ principleId,
96
+ decisionState: 'blocked',
97
+ learnedPrinciple: { status: 'unknown', reasonText: '该条目的决策投影暂时无法生成。' },
98
+ inbox: {
99
+ group: 'blocked', attention: 'individual',
100
+ reason: { code: 'projection_error', ownerText: '该条目的决策投影暂时无法生成。', sourceRefs: [] },
101
+ },
102
+ blockerCodes: ['projection_error'],
103
+ nextAction: { code: 'inspect_data', ownerText: '查看该原则详情页的错误说明。', targetRefs: [] },
104
+ });
105
+ }
106
+ }
107
+ return {
108
+ schemaVersion: '1',
109
+ generatedAt: now,
110
+ totalPrinciples: total,
111
+ groups: { decision, blocked, recovery },
112
+ historicalUnknown: {
113
+ count: historicalUnknown,
114
+ reasonText: historicalUnknown > 0
115
+ ? `有 ${historicalUnknown} 条历史原则的决策状态暂时无法确认(治理关联尚未建立)。这不是待办事项,也不代表需要你立即处理。`
116
+ : '',
117
+ },
118
+ };
119
+ }
120
+ /** Compact per-principle projection for the Library list (§11.2). */
121
+ async getLibraryCompact(now = ISO_NOW()) {
122
+ const snapshot = this.readBatchSnapshot();
123
+ let ledgerParsed;
124
+ try {
125
+ ledgerParsed = this.parseLedger();
126
+ }
127
+ catch {
128
+ return { schemaVersion: '1', generatedAt: now, entries: {}, degraded: { reason: 'principle ledger unavailable' } };
129
+ }
130
+ const principlesTree = GovernanceProjectionCollector.principleTreeFromLedger(ledgerParsed);
131
+ if (principlesTree === null) {
132
+ return { schemaVersion: '1', generatedAt: now, entries: {}, degraded: { reason: 'principle ledger unavailable' } };
133
+ }
134
+ const entries = {};
135
+ for (const principleId of Object.keys(principlesTree).sort()) {
136
+ try {
137
+ const view = deriveOwnerDecisionView(this.buildInputsFor({ principleId, ledgerParsed, snapshot, now, compact: true }));
138
+ entries[principleId] = {
139
+ decisionState: view.decisionState,
140
+ learnedPrinciple: view.learnedPrinciple.status === 'known'
141
+ ? { status: 'known', text: view.learnedPrinciple.value.text, sourceTier: view.learnedPrinciple.value.sourceTier }
142
+ : { status: 'unknown', reasonText: view.learnedPrinciple.reason.ownerText },
143
+ };
144
+ }
145
+ catch {
146
+ // Skip unreadable entries — the Library falls back to neutral copy.
147
+ }
148
+ }
149
+ return { schemaVersion: '1', generatedAt: now, entries };
150
+ }
151
+ // ── input assembly ────────────────────────────────────────────────────────
152
+ async collectInputs(principleId, now) {
153
+ const snapshot = this.readBatchSnapshot();
154
+ const ledgerParsed = this.parseLedger();
155
+ return this.buildInputsFor({ principleId, ledgerParsed, snapshot, now, compact: false });
156
+ }
157
+ parseLedger() {
158
+ return GovernanceProjectionCollector.parsePrincipleLedgerFile(this.workspaceDir);
159
+ }
160
+ buildInputsFor(input) {
161
+ const { principleId, ledgerParsed, snapshot, now, compact } = input;
162
+ const issues = [];
163
+ const principle = GovernanceProjectionCollector.principleFactFromLedger(ledgerParsed, principleId, issues);
164
+ const entry = this.ledgerEntryFor(ledgerParsed, principleId);
165
+ const sourceReads = [];
166
+ let governance;
167
+ if (!snapshot.dbAvailable) {
168
+ sourceReads.push({ source: 'state.db', status: 'unavailable', capturedAt: now, scope: 'governance tables', reason: snapshot.ledgerUnavailableReason });
169
+ governance = GovernanceProjectionCollector.factsForUnavailableSource({
170
+ principleId, asOf: now, principle, collectionIssues: issues,
171
+ });
172
+ }
173
+ else {
174
+ sourceReads.push({ source: 'state.db', status: 'available', capturedAt: now, scope: 'pi_artifacts/tasks/approvals/activations' });
175
+ governance = GovernanceProjectionCollector.buildFacts({
176
+ principleId, asOf: now, principle,
177
+ tables: {
178
+ artifactRows: snapshot.artifactRows,
179
+ taskRows: snapshot.taskRows,
180
+ approvalRows: snapshot.approvalRows,
181
+ activationRows: snapshot.activationRows,
182
+ },
183
+ collectionIssues: issues,
184
+ });
185
+ }
186
+ // F5-compatible reference classification: derivedFromPainIds may hold
187
+ // candidate UUIDs (current producer) or real Pain IDs (legacy writers).
188
+ // Only ids that resolve to principle_candidates become candidate refs —
189
+ // presence of an id never implies "it is a candidate".
190
+ const resolvedCandidates = [];
191
+ let ambiguousReferences = false;
192
+ for (const refId of entry.derivedFromPainIds) {
193
+ const hit = snapshot.candidates.get(refId);
194
+ if (hit !== undefined)
195
+ resolvedCandidates.push(hit);
196
+ }
197
+ if (resolvedCandidates.length > 1)
198
+ ambiguousReferences = true;
199
+ const candidate = resolvedCandidates.length === 1 ? resolvedCandidates[0] : undefined;
200
+ if (candidate === undefined && !compact) {
201
+ sourceReads.push({
202
+ source: 'principle_candidates',
203
+ status: ambiguousReferences ? 'unavailable' : 'not_requested',
204
+ capturedAt: now,
205
+ scope: 'candidate by derivedFromPainIds',
206
+ reason: ambiguousReferences ? 'multiple candidates resolve — refusing to guess (F5)' : 'no candidate resolved for the stored references',
207
+ });
208
+ }
209
+ else if (candidate !== undefined && !compact) {
210
+ sourceReads.push({ source: 'principle_candidates', status: 'available', capturedAt: now, scope: `candidate ${candidate.candidateId}` });
211
+ }
212
+ // Diagnosis read-through (diagnostic axis; NOT governance lineage).
213
+ let diagnosis = null;
214
+ if (!compact && candidate?.artifactId !== undefined) {
215
+ const artifact = snapshot.genericArtifacts.get(candidate.artifactId);
216
+ if (artifact?.contentJson !== undefined) {
217
+ diagnosis = this.parseDiagnosis(artifact.artifactId, artifact.contentJson);
218
+ sourceReads.push({ source: 'artifacts.diagnostician_output', status: diagnosis !== null ? 'available' : 'unavailable', capturedAt: now, scope: `artifact ${artifact.artifactId}`, reason: diagnosis === null ? 'contentJson not a DiagnosticianOutput shape' : undefined });
219
+ }
220
+ else {
221
+ sourceReads.push({ source: 'artifacts.diagnostician_output', status: 'unavailable', capturedAt: now, scope: `artifact ${candidate.artifactId}`, reason: 'generic artifact missing or unreadable' });
222
+ }
223
+ }
224
+ // Scribe + philosopher materials from PI artifacts (governance axis).
225
+ const semantic = this.readSemanticArtifacts(principleId, snapshot, compact);
226
+ const { scribe, philosopher } = semantic;
227
+ // Behavior applications (existing receipt ledger; read-only aggregate).
228
+ // Codex review P2 fix: a missing/unreadable principle_applications table
229
+ // must surface as an UNAVAILABLE source — an empty map with dbAvailable
230
+ // true would present "available, zero retained" on a damaged workspace.
231
+ let applications = null;
232
+ if (!compact && snapshot.dbAvailable && !snapshot.applicationsAvailable) {
233
+ sourceReads.push({ source: 'principle_applications', status: 'unavailable', capturedAt: now, scope: 'level+kind aggregates', reason: 'principle_applications table missing or unreadable' });
234
+ }
235
+ else if (!compact && snapshot.dbAvailable) {
236
+ const aggregated = snapshot.applicationsByPrinciple.get(principleId);
237
+ if (aggregated !== undefined) {
238
+ applications = {
239
+ sourceStatus: 'available',
240
+ deterministicCount: aggregated.deterministic,
241
+ selfReportedCount: aggregated.selfReported,
242
+ contextPresenceCount: aggregated.presence,
243
+ windowDays: 90,
244
+ recent: aggregated.recent,
245
+ };
246
+ sourceReads.push({ source: 'principle_applications', status: 'available', capturedAt: now, scope: 'level+kind aggregates + recent 5' });
247
+ }
248
+ else {
249
+ applications = { sourceStatus: 'available', deterministicCount: 0, selfReportedCount: 0, contextPresenceCount: 0, windowDays: 90, recent: [] };
250
+ sourceReads.push({ source: 'principle_applications', status: 'available', capturedAt: now, scope: 'level+kind aggregates (none retained)' });
251
+ }
252
+ }
253
+ // Semantic sources (lineage-constrained, tier-ordered inputs for the
254
+ // core selector; the collector only FILTERS by binding, never by text).
255
+ const semanticSources = [];
256
+ if (scribe?.statement !== undefined) {
257
+ semanticSources.push({ tier: 'scribe', text: scribe.statement, sourceVersion: scribe.artifactId });
258
+ }
259
+ if (candidate?.abstractedPrinciple !== undefined && candidate.abstractedPrinciple.trim() !== '') {
260
+ semanticSources.push({ tier: 'distiller', text: candidate.abstractedPrinciple, sourceVersion: candidate.candidateId });
261
+ }
262
+ if (philosopher?.title !== undefined) {
263
+ semanticSources.push({ tier: 'philosopher', text: philosopher.title, sourceVersion: philosopher.artifactId });
264
+ }
265
+ if (candidate !== undefined && candidate.recommendationKind === 'principle') {
266
+ semanticSources.push({ tier: 'candidate_principle', text: candidate.description, sourceVersion: candidate.candidateId });
267
+ }
268
+ const piRootBound = snapshot.dbAvailable
269
+ && snapshot.artifactRows.some((row) => isRecord(row) && readOwnString(row, 'source_principle_id') === principleId);
270
+ sourceReads.push({
271
+ source: 'ledger',
272
+ status: 'available',
273
+ capturedAt: now,
274
+ scope: 'principle entry',
275
+ });
276
+ return {
277
+ schemaVersion: '1',
278
+ principleId,
279
+ asOf: now,
280
+ ledger: {
281
+ status: entry.status,
282
+ text: entry.text,
283
+ evaluability: entry.evaluability,
284
+ triggerPattern: entry.triggerPattern,
285
+ action: entry.action,
286
+ derivedFromPainIds: entry.derivedFromPainIds,
287
+ ruleIds: entry.ruleIds,
288
+ createdAt: entry.createdAt,
289
+ updatedAt: entry.updatedAt,
290
+ },
291
+ governance,
292
+ candidate: candidate === undefined ? null : {
293
+ candidateId: candidate.candidateId,
294
+ status: candidate.status,
295
+ recommendationKind: candidate.recommendationKind,
296
+ description: candidate.description,
297
+ ...(candidate.abstractedPrinciple === undefined ? {} : { abstractedPrinciple: candidate.abstractedPrinciple }),
298
+ ...(candidate.trigger === undefined ? {} : { trigger: candidate.trigger }),
299
+ ...(candidate.action === undefined ? {} : { action: candidate.action }),
300
+ ...(candidate.title === undefined ? {} : { title: candidate.title }),
301
+ ...(candidate.confidence === undefined ? {} : { confidence: candidate.confidence }),
302
+ ...(candidate.artifactId === undefined ? {} : { sourceRef: candidate.artifactId }),
303
+ },
304
+ diagnosis: diagnosis === null ? null : {
305
+ artifactId: diagnosis.artifactId,
306
+ ...(diagnosis.summary === undefined ? {} : { summary: diagnosis.summary }),
307
+ ...(diagnosis.rootCause === undefined ? {} : { rootCause: diagnosis.rootCause }),
308
+ evidenceItems: [],
309
+ truncated: diagnosis.truncated,
310
+ },
311
+ scribe: scribe === null ? null : {
312
+ artifactId: scribe.artifactId,
313
+ ...(scribe.statement === undefined ? {} : { statement: scribe.statement }),
314
+ ...(scribe.rationale === undefined ? {} : { rationale: scribe.rationale }),
315
+ applicability: scribe.applicability,
316
+ antiPatterns: scribe.antiPatterns,
317
+ risks: scribe.risks,
318
+ ...(scribe.targetBehavior === undefined ? {} : { targetBehavior: scribe.targetBehavior }),
319
+ updatedAt: scribe.updatedAt,
320
+ },
321
+ philosopher: philosopher === null ? null : {
322
+ artifactId: philosopher.artifactId,
323
+ ...(philosopher.title === undefined ? {} : { title: philosopher.title }),
324
+ ...(philosopher.rationale === undefined ? {} : { rationale: philosopher.rationale }),
325
+ ...(philosopher.scope === undefined ? {} : { scope: philosopher.scope }),
326
+ },
327
+ applications,
328
+ coverageSourceStatus: !snapshot.dbAvailable ? 'unavailable' : 'available',
329
+ semanticSources,
330
+ technicalRecommendationAvailable: candidate !== undefined
331
+ && candidate.recommendationKind !== 'principle'
332
+ && candidate.description.trim() !== '',
333
+ // Fix 1 (review P1): per-artifact scribe materials — subject material
334
+ // resolves against this list by the subject's own artifactId; unrelated
335
+ // revisions never borrow each other's text.
336
+ scribeMaterials: semantic.scribeMaterials.map((material) => ({
337
+ artifactId: material.artifactId,
338
+ statement: material.statement ?? '',
339
+ ...(material.rationale === undefined ? {} : { rationale: material.rationale }),
340
+ ...(material.targetBehavior === undefined ? {} : { targetBehavior: material.targetBehavior }),
341
+ sourceVersion: material.artifactId,
342
+ })),
343
+ piRootBound,
344
+ sourceReads,
345
+ };
346
+ }
347
+ ledgerEntryFor(ledgerParsed, principleId) {
348
+ const tree = GovernanceProjectionCollector.principleTreeFromLedger(ledgerParsed);
349
+ const raw = tree?.[principleId];
350
+ if (!isRecord(raw))
351
+ throw new Error('principle_not_found');
352
+ const stringArray = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []);
353
+ return {
354
+ status: readOwnString(raw, 'status') ?? 'candidate',
355
+ text: readOwnString(raw, 'text') ?? '',
356
+ evaluability: readOwnString(raw, 'evaluability') ?? 'weak_heuristic',
357
+ triggerPattern: readOwnString(raw, 'triggerPattern') ?? '',
358
+ action: readOwnString(raw, 'action') ?? '',
359
+ derivedFromPainIds: stringArray(raw.derivedFromPainIds),
360
+ ruleIds: stringArray(raw.ruleIds),
361
+ createdAt: readOwnString(raw, 'createdAt') ?? '',
362
+ updatedAt: readOwnString(raw, 'updatedAt') ?? '',
363
+ };
364
+ }
365
+ parseDiagnosis(artifactId, contentJson) {
366
+ let parsed;
367
+ try {
368
+ parsed = JSON.parse(contentJson);
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ if (!isRecord(parsed))
374
+ return null;
375
+ const summary = readOwnString(parsed, 'summary');
376
+ const rootCause = readOwnString(parsed, 'rootCause');
377
+ if (summary === undefined && rootCause === undefined)
378
+ return null;
379
+ const rawText = JSON.stringify(parsed);
380
+ return {
381
+ artifactId,
382
+ ...(summary === undefined ? {} : { summary }),
383
+ ...(rootCause === undefined ? {} : { rootCause }),
384
+ truncated: rawText.includes('___TRUNCATED___'),
385
+ };
386
+ }
387
+ readSemanticArtifacts(principleId, snapshot, compact) {
388
+ let scribe = null;
389
+ const scribeMaterials = [];
390
+ let philosopher = null;
391
+ if (!snapshot.dbAvailable)
392
+ return { scribe, philosopher, scribeMaterials };
393
+ // Content rows are a separate targeted read: the shared governance batch
394
+ // SELECT intentionally omits content_json (heavy) — the semantic read-through
395
+ // needs it only for artifacts bound to THIS principle.
396
+ const boundRows = snapshot.piArtifactContentRows.filter((row) => isRecord(row) && readOwnString(row, 'source_principle_id') === principleId);
397
+ for (const row of boundRows) {
398
+ if (!isRecord(row))
399
+ continue;
400
+ const artifactId = readOwnString(row, 'artifact_id');
401
+ const sourceTaskId = readOwnString(row, 'source_task_id');
402
+ const updatedAt = readOwnString(row, 'updated_at') ?? '';
403
+ const contentJson = readOwnString(row, 'content_json');
404
+ if (artifactId === undefined || sourceTaskId === undefined || contentJson === undefined)
405
+ continue;
406
+ const taskKind = snapshot.piArtifactTaskKinds.get(sourceTaskId);
407
+ if (taskKind === 'scribe') {
408
+ const material = parseScribeContent(artifactId, contentJson, updatedAt);
409
+ if (material === null)
410
+ continue;
411
+ // Fix 1 (review P1): EVERY bound scribe artifact with a statement is
412
+ // per-artifact decision material — subject material resolves against
413
+ // this list by the subject's own artifactId, never borrowed across
414
+ // revisions.
415
+ scribeMaterials.push(material);
416
+ if (scribe === null || material.updatedAt >= scribe.updatedAt)
417
+ scribe = material;
418
+ }
419
+ }
420
+ if (!compact) {
421
+ // Philosopher tier needs an exact lineage relation: the artifact must be
422
+ // inside the bound closure (lineage_artifact_ids from a bound artifact).
423
+ const philosopherRows = this.philosopherRowsInBoundClosure(snapshot, boundRows);
424
+ for (const row of philosopherRows) {
425
+ if (!isRecord(row))
426
+ continue;
427
+ const artifactId = readOwnString(row, 'artifact_id');
428
+ const contentJson = readOwnString(row, 'content_json');
429
+ if (artifactId === undefined || contentJson === undefined)
430
+ continue;
431
+ const material = parsePhilosopherContent(artifactId, contentJson);
432
+ if (material !== null)
433
+ philosopher = material;
434
+ }
435
+ }
436
+ return { scribe, philosopher, scribeMaterials };
437
+ }
438
+ piContentRowsById(snapshot) {
439
+ const rowsById = new Map();
440
+ for (const row of snapshot.piArtifactContentRows) {
441
+ if (isRecord(row)) {
442
+ const id = readOwnString(row, 'artifact_id');
443
+ if (id !== undefined)
444
+ rowsById.set(id, row);
445
+ }
446
+ }
447
+ return rowsById;
448
+ }
449
+ philosopherRowsInBoundClosure(snapshot, boundRows) {
450
+ const closure = new Set();
451
+ const queue = boundRows
452
+ .filter(isRecord)
453
+ .map((row) => readOwnString(row, 'artifact_id'))
454
+ .filter((id) => id !== undefined);
455
+ const rowsById = this.piContentRowsById(snapshot);
456
+ while (queue.length > 0) {
457
+ const current = queue.shift();
458
+ if (current === undefined || closure.has(current))
459
+ continue;
460
+ closure.add(current);
461
+ const row = rowsById.get(current);
462
+ if (!isRecord(row))
463
+ continue;
464
+ const lineageJson = readOwnString(row, 'lineage_artifact_ids');
465
+ if (lineageJson === undefined)
466
+ continue;
467
+ try {
468
+ const parsed = JSON.parse(lineageJson);
469
+ if (Array.isArray(parsed)) {
470
+ for (const id of parsed) {
471
+ if (typeof id === 'string' && !closure.has(id))
472
+ queue.push(id);
473
+ }
474
+ }
475
+ }
476
+ catch {
477
+ // malformed lineage on one row — skip its expansion only
478
+ }
479
+ }
480
+ const result = [];
481
+ for (const artifactId of closure) {
482
+ const row = rowsById.get(artifactId);
483
+ if (!isRecord(row))
484
+ continue;
485
+ const sourceTaskId = readOwnString(row, 'source_task_id');
486
+ if (sourceTaskId !== undefined && snapshot.piArtifactTaskKinds.get(sourceTaskId) === 'philosopher') {
487
+ result.push(row);
488
+ }
489
+ }
490
+ return result;
491
+ }
492
+ // ── batch snapshot (shared reads; one DB open per request) ────────────────
493
+ readBatchSnapshot() {
494
+ const dbPath = path.join(this.workspaceDir, '.pd', 'state.db');
495
+ const dbAvailable = fs.existsSync(dbPath);
496
+ const candidates = new Map();
497
+ const genericArtifacts = new Map();
498
+ const piArtifactTaskKinds = new Map();
499
+ const applicationsByPrinciple = new Map();
500
+ let applicationsAvailable = true;
501
+ if (!dbAvailable) {
502
+ // No state.db: governance tables are unavailable; the view degrades with
503
+ // source_read_status=partial rather than pretending an empty world.
504
+ return {
505
+ artifactRows: [], taskRows: [], approvalRows: [], activationRows: [],
506
+ piArtifactContentRows: [],
507
+ candidates, genericArtifacts, piArtifactTaskKinds, applicationsByPrinciple,
508
+ applicationsAvailable: false,
509
+ dbAvailable: false,
510
+ ledgerUnavailableReason: 'state.db not found',
511
+ };
512
+ }
513
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
514
+ try {
515
+ const db = conn.getDb();
516
+ // One injected handle for the shared 4-table read + the extra sources.
517
+ const tables = GovernanceProjectionCollector.readTables(this.workspaceDir, conn);
518
+ // Targeted content read for the semantic axes (scribe/philosopher text):
519
+ // the shared governance SELECT omits content_json by design.
520
+ const piArtifactContentRows = db.prepare('SELECT artifact_id, source_task_id, source_principle_id, lineage_artifact_ids, content_json, updated_at FROM pi_artifacts ORDER BY artifact_id ASC').all();
521
+ for (const row of db.prepare('SELECT candidate_id, status, recommendation_kind, description, abstracted_principle, trigger_pattern, action, title, confidence, artifact_id FROM principle_candidates').all()) {
522
+ if (!isRecord(row))
523
+ continue;
524
+ const candidateId = readOwnString(row, 'candidate_id');
525
+ if (candidateId === undefined)
526
+ continue;
527
+ const confidence = Object.hasOwn(row, 'confidence') ? row.confidence : undefined;
528
+ candidates.set(candidateId, {
529
+ candidateId,
530
+ status: readOwnString(row, 'status') ?? 'unknown',
531
+ recommendationKind: readOwnString(row, 'recommendation_kind') ?? 'unknown',
532
+ description: readOwnString(row, 'description') ?? '',
533
+ ...(readOwnString(row, 'abstracted_principle') === undefined ? {} : { abstractedPrinciple: readOwnString(row, 'abstracted_principle') }),
534
+ ...(readOwnString(row, 'trigger_pattern') === undefined ? {} : { trigger: readOwnString(row, 'trigger_pattern') }),
535
+ ...(readOwnString(row, 'action') === undefined ? {} : { action: readOwnString(row, 'action') }),
536
+ ...(readOwnString(row, 'title') === undefined ? {} : { title: readOwnString(row, 'title') }),
537
+ ...(typeof confidence === 'number' ? { confidence } : {}),
538
+ ...(readOwnString(row, 'artifact_id') === undefined ? {} : { artifactId: readOwnString(row, 'artifact_id') }),
539
+ });
540
+ }
541
+ for (const row of db.prepare('SELECT artifact_id, artifact_kind, content_json FROM artifacts').all()) {
542
+ if (!isRecord(row))
543
+ continue;
544
+ const artifactId = readOwnString(row, 'artifact_id');
545
+ if (artifactId === undefined)
546
+ continue;
547
+ genericArtifacts.set(artifactId, {
548
+ artifactId,
549
+ kind: readOwnString(row, 'artifact_kind') ?? 'unknown',
550
+ ...(readOwnString(row, 'content_json') === undefined ? {} : { contentJson: readOwnString(row, 'content_json') }),
551
+ });
552
+ }
553
+ for (const row of tables.taskRows) {
554
+ if (!isRecord(row))
555
+ continue;
556
+ const taskId = readOwnString(row, 'task_id');
557
+ const taskKind = readOwnString(row, 'task_kind');
558
+ if (taskId !== undefined && taskKind !== undefined)
559
+ piArtifactTaskKinds.set(taskId, taskKind);
560
+ }
561
+ try {
562
+ const grouped = db.prepare('SELECT principle_id, level, kind, COUNT(*) AS n FROM principle_applications GROUP BY principle_id, level, kind').all();
563
+ for (const row of grouped) {
564
+ if (!isRecord(row))
565
+ continue;
566
+ const principleId = readOwnString(row, 'principle_id');
567
+ const level = readOwnString(row, 'level');
568
+ const kind = readOwnString(row, 'kind');
569
+ const n = Object.hasOwn(row, 'n') && typeof row.n === 'number' ? row.n : 0;
570
+ if (principleId === undefined || level === undefined || kind === undefined)
571
+ continue;
572
+ const bucket = applicationsByPrinciple.get(principleId) ?? { deterministic: 0, selfReported: 0, presence: 0, recent: [] };
573
+ if (level === 'effect' && (kind === 'rule_blocked' || kind === 'auto_correct_applied'))
574
+ bucket.deterministic += n;
575
+ else if (level === 'effect' && kind === 'self_reported')
576
+ bucket.selfReported += n;
577
+ else if (level === 'presence')
578
+ bucket.presence += n;
579
+ applicationsByPrinciple.set(principleId, bucket);
580
+ }
581
+ const recent = db.prepare('SELECT principle_id, level, kind, digest, created_at FROM principle_applications ORDER BY created_at DESC LIMIT 250').all();
582
+ for (const row of recent) {
583
+ if (!isRecord(row))
584
+ continue;
585
+ const principleId = readOwnString(row, 'principle_id');
586
+ if (principleId === undefined)
587
+ continue;
588
+ const bucket = applicationsByPrinciple.get(principleId) ?? { deterministic: 0, selfReported: 0, presence: 0, recent: [] };
589
+ if (bucket.recent.length < 5) {
590
+ bucket.recent.push({
591
+ text: `${readOwnString(row, 'kind') ?? 'observation'}${readOwnString(row, 'digest') !== undefined ? `:${(readOwnString(row, 'digest') ?? '').slice(0, 120)}` : ''}`,
592
+ ...(readOwnString(row, 'created_at') === undefined ? {} : { createdAt: readOwnString(row, 'created_at') }),
593
+ level: readOwnString(row, 'level') ?? 'unknown',
594
+ });
595
+ }
596
+ }
597
+ }
598
+ catch {
599
+ // Codex review P2: a missing/unreadable ledger table is an UNAVAILABLE
600
+ // source, not an available-with-zero source (Codex P2 fix).
601
+ applicationsAvailable = false;
602
+ }
603
+ return {
604
+ ...tables,
605
+ piArtifactContentRows,
606
+ candidates,
607
+ genericArtifacts,
608
+ piArtifactTaskKinds,
609
+ applicationsByPrinciple,
610
+ applicationsAvailable,
611
+ dbAvailable: true,
612
+ };
613
+ }
614
+ finally {
615
+ conn.close();
616
+ }
617
+ }
618
+ }
619
+ // ── PI artifact content parsers (rc-1/rc-2: untrusted JSON, validated) ─────
620
+ function parseScribeContent(artifactId, contentJson, updatedAt) {
621
+ let parsed;
622
+ try {
623
+ parsed = JSON.parse(contentJson);
624
+ }
625
+ catch {
626
+ return null;
627
+ }
628
+ if (!isRecord(parsed))
629
+ return null;
630
+ const draft = parsed.principleDraft;
631
+ const draftRecord = isRecord(draft) ? draft : undefined;
632
+ // Codex P1 follow-up: a scribe artifact WITHOUT a statement carries no
633
+ // owner-facing principle material — it must not be selected as "the" scribe
634
+ // material for the principle (that would shadow a real one and would let
635
+ // per-subject gating treat its revision as scribe-backed).
636
+ if (draftRecord === undefined || readOwnString(draftRecord, 'statement') === undefined)
637
+ return null;
638
+ const risksValue = parsed.risks;
639
+ const risks = Array.isArray(risksValue) ? risksValue.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
640
+ const applicability = draftRecord !== undefined && Array.isArray(draftRecord.applicability)
641
+ ? draftRecord.applicability.filter((item) => typeof item === 'string')
642
+ : [];
643
+ const antiPatterns = draftRecord !== undefined && Array.isArray(draftRecord.antiPatterns)
644
+ ? draftRecord.antiPatterns.filter((item) => typeof item === 'string')
645
+ : [];
646
+ const intentContract = isRecord(parsed.intentContract) ? parsed.intentContract : undefined;
647
+ const targetBehavior = intentContract !== undefined ? readOwnString(intentContract, 'targetBehavior') : undefined;
648
+ return {
649
+ artifactId,
650
+ statement: readOwnString(draftRecord, 'statement'),
651
+ ...(draftRecord !== undefined && readOwnString(draftRecord, 'rationale') !== undefined ? { rationale: readOwnString(draftRecord, 'rationale') } : {}),
652
+ applicability,
653
+ antiPatterns,
654
+ risks,
655
+ ...(targetBehavior === undefined ? {} : { targetBehavior }),
656
+ updatedAt,
657
+ };
658
+ }
659
+ function parsePhilosopherContent(artifactId, contentJson) {
660
+ let parsed;
661
+ try {
662
+ parsed = JSON.parse(contentJson);
663
+ }
664
+ catch {
665
+ return null;
666
+ }
667
+ if (!isRecord(parsed))
668
+ return null;
669
+ const candidateRecord = isRecord(parsed.principleCandidate) ? parsed.principleCandidate : undefined;
670
+ if (candidateRecord === undefined)
671
+ return null;
672
+ return {
673
+ artifactId,
674
+ ...(readOwnString(candidateRecord, 'title') === undefined ? {} : { title: readOwnString(candidateRecord, 'title') }),
675
+ ...(readOwnString(candidateRecord, 'rationale') === undefined ? {} : { rationale: readOwnString(candidateRecord, 'rationale') }),
676
+ ...(readOwnString(candidateRecord, 'scope') === undefined ? {} : { scope: readOwnString(candidateRecord, 'scope') }),
677
+ };
678
+ }
679
+ // Compact semantic title helper for the Library list (same selector, no
680
+ // second eligibility algorithm).
681
+ export function compactSemanticTitle(view) {
682
+ if (view.learnedPrinciple.status === 'known') {
683
+ return { status: 'known', text: view.learnedPrinciple.value.text, sourceTier: view.learnedPrinciple.value.sourceTier };
684
+ }
685
+ return { status: 'unknown', reasonText: view.learnedPrinciple.reason.ownerText };
686
+ }