create-principles-disciple 1.144.11 → 1.144.12

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 (92) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +1 -1
  3. package/console/dist/server/models/ApprovalsConsoleModel.js +4 -4
  4. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +5 -5
  5. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +18 -18
  6. package/console/dist/shared/prompt-injection-contract.d.ts +5 -5
  7. package/console/dist/ui/pages/focus/FocusPage.js +1 -1
  8. package/console/dist/ui/pages/focus/focus-validation.js +1 -1
  9. package/console/dist/ui/utils/approval-warning-localization.js +1 -1
  10. package/console/dist/ui/utils/validators.d.ts +1 -1
  11. package/console/dist/ui/utils/validators.js +2 -2
  12. package/console/package.json +1 -1
  13. package/core/dist/principle-tree-ledger.d.ts +50 -0
  14. package/core/dist/principle-tree-ledger.d.ts.map +1 -1
  15. package/core/dist/principle-tree-ledger.js +133 -1
  16. package/core/dist/principle-tree-ledger.js.map +1 -1
  17. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts +24 -0
  18. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts.map +1 -1
  19. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js +36 -1
  20. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js.map +1 -1
  21. package/core/dist/runtime-v2/candidate-intake-service.d.ts +102 -3
  22. package/core/dist/runtime-v2/candidate-intake-service.d.ts.map +1 -1
  23. package/core/dist/runtime-v2/candidate-intake-service.js +280 -47
  24. package/core/dist/runtime-v2/candidate-intake-service.js.map +1 -1
  25. package/core/dist/runtime-v2/candidate-intake.d.ts +44 -0
  26. package/core/dist/runtime-v2/candidate-intake.d.ts.map +1 -1
  27. package/core/dist/runtime-v2/candidate-intake.js +7 -0
  28. package/core/dist/runtime-v2/candidate-intake.js.map +1 -1
  29. package/core/dist/runtime-v2/index.d.ts +6 -0
  30. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  31. package/core/dist/runtime-v2/index.js +7 -0
  32. package/core/dist/runtime-v2/index.js.map +1 -1
  33. package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
  34. package/core/dist/runtime-v2/pain-signal-bridge.js +14 -1
  35. package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
  36. package/core/dist/runtime-v2/principle-reuse/reuse-domain.d.ts +89 -0
  37. package/core/dist/runtime-v2/principle-reuse/reuse-domain.d.ts.map +1 -0
  38. package/core/dist/runtime-v2/principle-reuse/reuse-domain.js +16 -0
  39. package/core/dist/runtime-v2/principle-reuse/reuse-domain.js.map +1 -0
  40. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.d.ts +87 -0
  41. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.d.ts.map +1 -0
  42. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.js +99 -0
  43. package/core/dist/runtime-v2/principle-reuse/reuse-proposal.js.map +1 -0
  44. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.d.ts +36 -0
  45. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.d.ts.map +1 -0
  46. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.js +118 -0
  47. package/core/dist/runtime-v2/principle-reuse/reuse-retrieval.js.map +1 -0
  48. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.d.ts +40 -0
  49. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.d.ts.map +1 -0
  50. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.js +184 -0
  51. package/core/dist/runtime-v2/principle-reuse/reuse-shortlist.js.map +1 -0
  52. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts +7 -0
  53. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.d.ts.map +1 -1
  54. package/core/dist/runtime-v2/store/pain-diagnosis/pain-diagnosis-store.js.map +1 -1
  55. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts +1 -0
  56. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.d.ts.map +1 -1
  57. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js +12 -0
  58. package/core/dist/runtime-v2/store/pain-diagnosis/sqlite-pain-diagnosis-store.js.map +1 -1
  59. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts +5 -0
  60. package/core/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
  61. package/core/dist/runtime-v2/store/runtime-state-manager.js +8 -0
  62. package/core/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
  63. package/core/dist/runtime-v2/types/principle-schema.d.ts +65 -0
  64. package/core/dist/runtime-v2/types/principle-schema.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/types/principle-schema.js +14 -0
  66. package/core/dist/runtime-v2/types/principle-schema.js.map +1 -1
  67. package/core/dist/runtime-v2/types/principle-tree-store.d.ts +12 -0
  68. package/core/dist/runtime-v2/types/principle-tree-store.d.ts.map +1 -1
  69. package/core/dist/telemetry-event.d.ts +2 -2
  70. package/core/dist/telemetry-event.d.ts.map +1 -1
  71. package/core/dist/telemetry-event.js +2 -0
  72. package/core/dist/telemetry-event.js.map +1 -1
  73. package/core/package.json +2 -2
  74. package/host-runtime/dist/prompt-injection-projection.d.ts +5 -5
  75. package/host-runtime/dist/prompt-injection-projection.js +4 -4
  76. package/package.json +1 -1
  77. package/pd-cli/dist/commands/candidate.d.ts +27 -0
  78. package/pd-cli/dist/commands/candidate.d.ts.map +1 -1
  79. package/pd-cli/dist/commands/candidate.js +358 -14
  80. package/pd-cli/dist/commands/candidate.js.map +1 -1
  81. package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
  82. package/pd-cli/dist/commands/diagnose.js +17 -3
  83. package/pd-cli/dist/commands/diagnose.js.map +1 -1
  84. package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
  85. package/pd-cli/dist/commands/pain-retry.js +17 -3
  86. package/pd-cli/dist/commands/pain-retry.js.map +1 -1
  87. package/pd-cli/dist/index.js +16 -2
  88. package/pd-cli/dist/index.js.map +1 -1
  89. package/pd-cli/package.json +1 -1
  90. package/plugin/dist/bundle.js +473 -473
  91. package/plugin/package.json +2 -2
  92. package/release-manager/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "productVersion": "2.2.0",
4
- "sourceCommit": "9b993151f65b075845e56a4f19dd523eac9810da"
4
+ "sourceCommit": "332de78574839e4970145fc34617ed4b8abf743a"
5
5
  }
@@ -66,7 +66,7 @@ export declare class ApprovalsConsoleModel {
66
66
  * principles. A projection error is also surfaced (never silent) but must
67
67
  * not fail the approve.
68
68
  *
69
- * PRI-935 — the console holds no session, so it cannot supply the production
69
+ * PR-1894 — the console holds no session, so it cannot supply the production
70
70
  * round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
71
71
  * conservative forecast: FIFO structurally drops the newest activation from
72
72
  * every truncated selection, while the live plugin route rotates and injects
@@ -156,7 +156,7 @@ export class ApprovalsConsoleModel {
156
156
  // The prompt surface renders active activations under a hard char cap;
157
157
  // when the budget is saturated the newest Owner approval may not reach
158
158
  // agent behavior on THIS turn. Surface it as a non-fatal warning (rc-9)
159
- // instead — the activation stays committed. PRI-935: whether "not on this
159
+ // instead — the activation stays committed. PR-1894: whether "not on this
160
160
  // turn" means "queued behind rotation" or genuinely starved depends on the
161
161
  // selection policy, which checkPromptInjectionBudget now reports honestly.
162
162
  let injectionWarning;
@@ -182,7 +182,7 @@ export class ApprovalsConsoleModel {
182
182
  * principles. A projection error is also surfaced (never silent) but must
183
183
  * not fail the approve.
184
184
  *
185
- * PRI-935 — the console holds no session, so it cannot supply the production
185
+ * PR-1894 — the console holds no session, so it cannot supply the production
186
186
  * round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
187
187
  * conservative forecast: FIFO structurally drops the newest activation from
188
188
  * every truncated selection, while the live plugin route rotates and injects
@@ -211,7 +211,7 @@ export class ApprovalsConsoleModel {
211
211
  // that instead of blaming the budget.
212
212
  return `injection_excluded_non_budget: the activation is committed but excluded from the prompt injection projection for a non-budget reason.${projectionDetail} nextAction=inspect the artifact/activation pair via pd runtime activation list`;
213
213
  }
214
- // PRI-935: fair rotation is the live plugin-local policy (PRI-904). An
214
+ // PR-1894: fair rotation is the live plugin-local policy (PRI-904). An
215
215
  // activation that SOME round does inject is rotated out of the current
216
216
  // window, not starved — report that bounded fact instead of the old FIFO
217
217
  // starvation claim. The shared route genuinely does not rotate, so its
@@ -220,7 +220,7 @@ export class ApprovalsConsoleModel {
220
220
  const window = projection.eligibleCount;
221
221
  return `injection_budget_queued: the activation is committed but sits outside the CURRENT prompt injection window (${projection.budget}c; ${projection.injectedActivationIds.length} of ${window} eligible activations injected this round; production selection policy fair_rotation_v1). Fair rotation advances the window by one position per recorded user turn, so this principle WILL enter agent behavior within at most ${window} consecutive user turns of a continuously advancing session — deactivating older principles is NOT required. nextAction=none; verify presence via the activations page or the prompt injection telemetry`;
222
222
  }
223
- // PRI-935: report the PRODUCTION policy, not the policy this forecast
223
+ // PR-1894: report the PRODUCTION policy, not the policy this forecast
224
224
  // happened to run. The console holds no session round key, so its own
225
225
  // projection is always `legacy_fifo_prefix_v1` even on a route where
226
226
  // production rotates — labelling that value "the current production
@@ -8,9 +8,9 @@ export interface ApprovalGroup {
8
8
  candidateDescription?: string;
9
9
  status: 'pending' | 'approved' | 'rejected';
10
10
  /**
11
- * PRI-935: whether this candidate's own serialized prompt entry fits the
11
+ * PR-1894: whether this candidate's own serialized prompt entry fits the
12
12
  * injection budget on its own. `false` means the entry is oversized, so fair
13
- * rotation can NEVER inject it �?the pre-approval badge must not promise it
13
+ * rotation can NEVER inject it �?the pre-approval badge must not promise it
14
14
  * will come around. Computed server-side with the production serializer
15
15
  * (the client has no access to the escaped, serialized size).
16
16
  */
@@ -36,11 +36,11 @@ export declare class ApprovalsGroupedConsoleModel {
36
36
  getApprovalsGrouped(): Promise<ApprovalsGroupedResponse>;
37
37
  /**
38
38
  * PRI-908: recompute the production prompt injection projection so the
39
- * focus page can forecast "approved �?effective" BEFORE the Owner decides.
39
+ * focus page can forecast "approved �?effective" BEFORE the Owner decides.
40
40
  * PR #1844 follow-up: the forecast follows the workspace's REAL injection
41
- * route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
41
+ * route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
42
42
  * shared render via abstraction_layer_v1), matching the PRI-890 approve-time
43
- * check. Advisory only: a projection failure omits the field �?the
43
+ * check. Advisory only: a projection failure omits the field �?the
44
44
  * approve-time warning remains the fail-loud exclusion report (rc-9), so
45
45
  * this must never fail the grouped read.
46
46
  */
@@ -17,7 +17,7 @@ function isMissingTableError(err) {
17
17
  /**
18
18
  * Wave 7: Extract a human-readable description from a PIArtifact's contentJson.
19
19
  *
20
- * The artifact kinds have different contentJson shapes �?this function unifies
20
+ * The artifact kinds have different contentJson shapes �?this function unifies
21
21
  * them into a single description string the Owner can actually read to make a
22
22
  * review decision, instead of staring at a fabricated principleId.
23
23
  *
@@ -50,7 +50,7 @@ function extractCandidateDescription(contentJson) {
50
50
  const principleText = principleRaw ? principleRaw.trim() : null;
51
51
  const ruleText = ruleRaw ? ruleRaw.trim() : null;
52
52
  if (principleText && ruleText)
53
- return `${principleText} �?${ruleText}`;
53
+ return `${principleText} �?${ruleText}`;
54
54
  if (ruleText)
55
55
  return ruleText;
56
56
  if (principleText)
@@ -65,7 +65,7 @@ function extractCandidateDescription(contentJson) {
65
65
  const title = typeof draft.title === 'string' ? draft.title.trim() : '';
66
66
  const statement = typeof draft.statement === 'string' ? draft.statement.trim() : '';
67
67
  if (title && statement)
68
- return `${title} �?${statement}`;
68
+ return `${title} �?${statement}`;
69
69
  if (title)
70
70
  return title;
71
71
  if (statement)
@@ -90,7 +90,7 @@ function extractCandidateDescription(contentJson) {
90
90
  return null;
91
91
  }
92
92
  /**
93
- * PRI-935: would this candidate's own prompt entry fit the injection budget if
93
+ * PR-1894: would this candidate's own prompt entry fit the injection budget if
94
94
  * it were the ONLY entry?
95
95
  *
96
96
  * Uses the production text resolver and the production entry format/escaping
@@ -101,13 +101,13 @@ function extractCandidateDescription(contentJson) {
101
101
  * fair rotation cannot deliver it.
102
102
  *
103
103
  * Degrades to `false` (do not promise rotation) whenever the real size cannot be
104
- * determined �?an unknown size must never become a promise (rc-9).
104
+ * determined �?an unknown size must never become a promise (rc-9).
105
105
  */
106
106
  function candidateFitsPromptBudget(artifact) {
107
107
  if (!artifact?.contentJson)
108
108
  return false;
109
109
  // The pending candidate has no activation row yet, but the resolver only
110
- // needs the artifact plus an id pair �?this mirrors what approve will do.
110
+ // needs the artifact plus an id pair �?this mirrors what approve will do.
111
111
  const resolved = resolvePrincipleFromArtifact({
112
112
  artifact_id: artifact.artifactId,
113
113
  artifact_kind: artifact.artifactKind,
@@ -139,7 +139,7 @@ export class ApprovalsGroupedConsoleModel {
139
139
  async getApprovalsGrouped() {
140
140
  const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
141
141
  if (!fs.existsSync(stateDbPath)) {
142
- return { groups: [], generatedAt: new Date().toISOString(), note: 'state.db not found �?workspace may not be initialized' };
142
+ return { groups: [], generatedAt: new Date().toISOString(), note: 'state.db not found �?workspace may not be initialized' };
143
143
  }
144
144
  const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
145
145
  try {
@@ -152,21 +152,21 @@ export class ApprovalsGroupedConsoleModel {
152
152
  }
153
153
  catch (err) {
154
154
  if (isMissingTableError(err)) {
155
- return { groups: [], generatedAt: new Date().toISOString(), note: 'approval table not found �?workspace may not be initialized' };
155
+ return { groups: [], generatedAt: new Date().toISOString(), note: 'approval table not found �?workspace may not be initialized' };
156
156
  }
157
157
  throw err;
158
158
  }
159
- // Build artifactId �?sourcePrincipleId map AND artifactId �?candidateDescription map.
159
+ // Build artifactId �?sourcePrincipleId map AND artifactId �?candidateDescription map.
160
160
  // Wave 7: candidateDescription lets FocusPage show human-readable content
161
161
  // instead of a fabricated principleId.
162
162
  //
163
163
  // PRI-768 v5 follow-up (F3): sourcePrincipleId is null for most scribe
164
164
  // artifacts, which used to collapse every group to `unlinked:<id>` and
165
165
  // dead-end the owner decision UI. When the column is missing, fall back
166
- // to the shared lineage resolver (scribe �?dreamer seed �?candidateId �? // ledger derivedFromPainIds) so groups bind to the REAL ledger id.
166
+ // to the shared lineage resolver (scribe �?dreamer seed �?candidateId �? // ledger derivedFromPainIds) so groups bind to the REAL ledger id.
167
167
  const artifactPrincipleMap = new Map();
168
168
  const artifactDescriptionMap = new Map();
169
- // PRI-935: per-artifact "does this entry fit the budget alone" fact, so
169
+ // PR-1894: per-artifact "does this entry fit the budget alone" fact, so
170
170
  // the pre-approval badge never promises rotation for an entry that is
171
171
  // oversized (rotation cannot rescue a per-entry overflow).
172
172
  const artifactFitsBudgetMap = new Map();
@@ -207,9 +207,9 @@ export class ApprovalsGroupedConsoleModel {
207
207
  }
208
208
  }
209
209
  catch {
210
- // Ledger not available �?will fall back to principleId
210
+ // Ledger not available �?will fall back to principleId
211
211
  }
212
- // Group by principleId (null �?"unlinked")
212
+ // Group by principleId (null �?"unlinked")
213
213
  const groupMap = new Map();
214
214
  for (const approval of allApprovals) {
215
215
  const mappedPrincipleId = artifactPrincipleMap.get(approval.artifactId);
@@ -253,7 +253,7 @@ export class ApprovalsGroupedConsoleModel {
253
253
  principleTitle,
254
254
  candidateDescription,
255
255
  status,
256
- // PRI-935: undefined-safe �?absent when the artifact could not be
256
+ // PR-1894: undefined-safe �?absent when the artifact could not be
257
257
  // read, which the UI treats as "do not promise rotation".
258
258
  ...(firstArtifactId !== undefined && artifactFitsBudgetMap.has(firstArtifactId)
259
259
  ? { fitsPromptBudget: artifactFitsBudgetMap.get(firstArtifactId) === true }
@@ -276,11 +276,11 @@ export class ApprovalsGroupedConsoleModel {
276
276
  }
277
277
  /**
278
278
  * PRI-908: recompute the production prompt injection projection so the
279
- * focus page can forecast "approved �?effective" BEFORE the Owner decides.
279
+ * focus page can forecast "approved �?effective" BEFORE the Owner decides.
280
280
  * PR #1844 follow-up: the forecast follows the workspace's REAL injection
281
- * route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
281
+ * route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
282
282
  * shared render via abstraction_layer_v1), matching the PRI-890 approve-time
283
- * check. Advisory only: a projection failure omits the field �?the
283
+ * check. Advisory only: a projection failure omits the field �?the
284
284
  * approve-time warning remains the fail-loud exclusion report (rc-9), so
285
285
  * this must never fail the grouped read.
286
286
  */
@@ -292,7 +292,7 @@ export class ApprovalsGroupedConsoleModel {
292
292
  budget: projection.budget,
293
293
  usedChars: projection.usedChars,
294
294
  truncated: projection.truncated,
295
- // PRI-935: the badge must describe what the PRODUCTION route will do,
295
+ // PR-1894: the badge must describe what the PRODUCTION route will do,
296
296
  // not which policy this session-less forecast happened to run.
297
297
  // Without it the UI can only say "will queue", which under fair
298
298
  // rotation reads as permanent starvation and pushes the Owner to
@@ -19,19 +19,19 @@ export interface PromptInjectionBudgetStatus {
19
19
  usedChars: number;
20
20
  /**
21
21
  * True when the CURRENT projection already truncated — the newly approved
22
- * principle may not enter the prompt on this turn. PRI-935: this is
22
+ * principle may not enter the prompt on this turn. PR-1894: this is
23
23
  * policy-dependent (see `selectionPolicy`); the badge copy must not imply
24
24
  * permanent starvation when rotation is in play.
25
25
  */
26
26
  truncated: boolean;
27
27
  /**
28
- * PRI-935: whether the PRODUCTION route rotates. This — not the forecast's
28
+ * PR-1894: whether the PRODUCTION route rotates. This — not the forecast's
29
29
  * own selection policy — decides whether an out-of-window approval is queued
30
30
  * behind rotation or genuinely starved, and therefore which badge copy is
31
- * truthful. Optional so a pre-PRI-935 payload still validates; absent
32
- * behaves as `false` (the conservative pre-PRI-935 reading).
31
+ * truthful. Optional so a pre-PR-1894 payload still validates; absent
32
+ * behaves as `false` (the conservative pre-PR-1894 reading).
33
33
  */
34
34
  productionRotates?: boolean;
35
- /** PRI-935: eligible activations that reached the selector. */
35
+ /** PR-1894: eligible activations that reached the selector. */
36
36
  eligibleCount?: number;
37
37
  }
@@ -148,7 +148,7 @@ function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, prom
148
148
  function notifySuccessWarnings(successWarnings) {
149
149
  const segments = successWarnings.flatMap((warning) => splitApprovalWarnings(warning));
150
150
  const localized = segments.map((warning) => localizeApprovalWarning(warning, t));
151
- // PRI-935: the raw server text is still preserved verbatim (rc-9 — never
151
+ // PR-1894: the raw server text is still preserved verbatim (rc-9 — never
152
152
  // dropped), but it is operator-facing English, not Owner-facing prose. It
153
153
  // used to be appended flat under the localized body, so the toast rendered
154
154
  // as one mixed Chinese/English block and the machine text visually
@@ -54,7 +54,7 @@ function validateApprovalGroup(raw) {
54
54
  return null;
55
55
  candidateDescription = description;
56
56
  }
57
- // PRI-935: fitsPromptBudget is optional. ERR-009 discipline — present but
57
+ // PR-1894: fitsPromptBudget is optional. ERR-009 discipline — present but
58
58
  // wrong type fails loud; absent stays undefined, which the badge treats as
59
59
  // "unknown size → do not promise rotation".
60
60
  let fitsPromptBudget;
@@ -65,7 +65,7 @@ export function localizeApprovalWarning(warning, t) {
65
65
  activationAction: true,
66
66
  };
67
67
  }
68
- // PRI-935: the activation is merely rotated out of the CURRENT window, not
68
+ // PR-1894: the activation is merely rotated out of the CURRENT window, not
69
69
  // starved. This is the common case on a live fair-rotation workspace and the
70
70
  // copy must NOT tell the Owner to deactivate healthy older principles.
71
71
  if (code === 'injection_budget_queued') {
@@ -637,7 +637,7 @@ export interface ApprovalGroupData {
637
637
  /** Wave 7: human-readable principle text from artifact contentJson, if available */
638
638
  candidateDescription?: string;
639
639
  /**
640
- * PRI-935: whether this candidate's own serialized prompt entry fits the
640
+ * PR-1894: whether this candidate's own serialized prompt entry fits the
641
641
  * injection budget alone. Absent = unknown, which the badge treats as
642
642
  * "do not promise rotation".
643
643
  */
@@ -1646,7 +1646,7 @@ function validateApprovalGroup(v) {
1646
1646
  if (Object.hasOwn(v, 'candidateDescription') && isString(v.candidateDescription) && v.candidateDescription.length > 0) {
1647
1647
  result.candidateDescription = v.candidateDescription;
1648
1648
  }
1649
- // PRI-935: optional fitsPromptBudget. Only a real boolean is propagated; a
1649
+ // PR-1894: optional fitsPromptBudget. Only a real boolean is propagated; a
1650
1650
  // present-but-wrong value is dropped (rc-1/rc-2) so the badge falls back to
1651
1651
  // copy that promises nothing rather than rendering an unverified claim.
1652
1652
  if (Object.hasOwn(v, 'fitsPromptBudget') && typeof v.fitsPromptBudget === 'boolean') {
@@ -1666,7 +1666,7 @@ export function validatePromptInjectionBudgetStatus(v) {
1666
1666
  return null;
1667
1667
  }
1668
1668
  const result = { budget, usedChars, truncated };
1669
- // PRI-935: these are optional so a pre-PRI-935 server payload still
1669
+ // PR-1894: these are optional so a pre-PR-1894 server payload still
1670
1670
  // validates (backward compatibility). A present-but-invalid value is
1671
1671
  // DROPPED, never propagated (rc-1/rc-2): the UI then falls back to
1672
1672
  // policy-agnostic copy rather than rendering an unverified policy claim.
@@ -22,7 +22,7 @@
22
22
  "verify:build": "node scripts/verify-build.mjs"
23
23
  },
24
24
  "dependencies": {
25
- "@fontsource/jetbrains-mono": "^5.2.8",
25
+ "@fontsource/jetbrains-mono": "^5.3.0",
26
26
  "@principles/core": "file:../core",
27
27
  "@principles/host-runtime": "file:../host-runtime",
28
28
  "@principles/install-layout": "file:../install-layout",
@@ -16,7 +16,9 @@
16
16
  import type { Principle, Rule, Implementation, PrincipleValueMetrics, LedgerPrinciple, LedgerRule, LedgerTreeStore, LegacyPrincipleTrainingState, LegacyPrincipleTrainingStore, HybridLedgerStore } from './runtime-v2/types/ledger-store.js';
17
17
  import { TREE_NAMESPACE } from './runtime-v2/types/ledger-store.js';
18
18
  import type { ImplementationLifecycleState } from './runtime-v2/types/principle-enums.js';
19
+ import type { ReuseEvidenceActor, ReuseEvidenceEntry } from './runtime-v2/types/principle-schema.js';
19
20
  export type { Principle, Rule, Implementation, PrincipleValueMetrics, LedgerPrinciple, LedgerRule, LedgerTreeStore, LegacyPrincipleTrainingState, LegacyPrincipleTrainingStore, HybridLedgerStore, ImplementationLifecycleState, };
21
+ export type { ReuseEvidenceActor, ReuseEvidenceEntry };
20
22
  export { TREE_NAMESPACE };
21
23
  export declare class LockAcquisitionError extends Error {
22
24
  readonly filePath: string;
@@ -43,6 +45,54 @@ export declare function loadLedger(stateDir: string): HybridLedgerStore;
43
45
  export declare function saveLedger(stateDir: string, store: HybridLedgerStore): void;
44
46
  export declare function addPrincipleToLedger(stateDir: string, principle: LedgerPrinciple): LedgerPrinciple;
45
47
  export declare function updatePrinciple(stateDir: string, principleId: string, updates: Partial<LedgerPrinciple>): LedgerPrinciple;
48
+ /**
49
+ * Thrown when the ledger file's content is not trustworthy for a read-modify-
50
+ * write: it parses to an empty principles tree while the file exists on disk
51
+ * (empty, corrupt, or truncated), or an entity's persisted reuseEvidence field
52
+ * has a malformed shape. Unlike a missing principle, writing over this state
53
+ * would DESTROY ledger content, so the write refuses instead of proceeding
54
+ * (rc-3).
55
+ */
56
+ export declare class LedgerIntegrityError extends Error {
57
+ readonly filePath: string;
58
+ constructor(message: string, filePath: string);
59
+ }
60
+ /**
61
+ * Append one reuse-evidence entry to an existing Principle (PRI-917 PR3B,
62
+ * SPEC v0.2.1 §12). The ONLY writer for `Principle.reuseEvidence`.
63
+ *
64
+ * Runs entirely inside the single-writer file lock via mutateLedger —
65
+ * read → integrity guard → dedupe → append → save — so a concurrent append
66
+ * can never be lost, and there is no load-then-update window between
67
+ * processes. Do NOT implement appends via addPrincipleToLedger (it replaces
68
+ * the whole entry and would silently drop existing evidence) or via an
69
+ * external loadLedger+updatePrinciple sequence (read outside the lock).
70
+ *
71
+ * Fail-closed:
72
+ * - a ledger file that exists but parses to an empty principles tree (empty
73
+ * file, corrupt, or truncated) is treated as corruption — the write
74
+ * refuses rather than risk replacing the corpus with the new entry alone
75
+ * (LedgerIntegrityError);
76
+ * - a missing principleId refuses the write (no file mutation);
77
+ * - a malformed entry refuses the write.
78
+ *
79
+ * Idempotency key: (principleId, entry.candidateId). A candidate resolves
80
+ * into a principle at most once, so replaying the same decision is a no-op
81
+ * that returns the existing evidence with `appended: false` (INV-R07). A
82
+ * no-op still goes through the normal save path, so the tree's lastUpdated
83
+ * is refreshed; the ledger content itself is unchanged.
84
+ *
85
+ * Append-only (INV-R05): no other Principle field — including updatedAt —
86
+ * is touched, and existing entries are never rewritten or removed. Target
87
+ * eligibility (e.g. archived/deprecated exclusion, SPEC §15) is the
88
+ * retrieval/proposal layer's invariant; this writer records the decision
89
+ * verbatim.
90
+ */
91
+ export declare function appendReuseEvidence(stateDir: string, principleId: string, entry: ReuseEvidenceEntry): {
92
+ principleId: string;
93
+ reuseEvidence: ReuseEvidenceEntry[];
94
+ appended: boolean;
95
+ };
46
96
  export declare function updatePrincipleValueMetrics(stateDir: string, principleId: string, metrics: PrincipleValueMetrics): PrincipleValueMetrics;
47
97
  export declare function getLedgerFilePathPublic(stateDir: string): string;
48
98
  export interface PrincipleSubtree {
@@ -1 +1 @@
1
- {"version":3,"file":"principle-tree-ledger.d.ts","sourceRoot":"","sources":["../src/principle-tree-ledger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"principle-tree-ledger.d.ts","sourceRoot":"","sources":["../src/principle-tree-ledger.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,7 @@ import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import { TREE_NAMESPACE } from './runtime-v2/types/ledger-store.js';
19
19
  // PRI-443: Pure parse/serialize functions extracted to codec module
20
- import { uniqueStrings, createEmptyTree, parseHybridLedger, serializeLedger, } from './runtime-v2/principle-tree/ledger-codec.js';
20
+ import { isRecord, uniqueStrings, createEmptyTree, parseHybridLedger, serializeLedger, } from './runtime-v2/principle-tree/ledger-codec.js';
21
21
  export { TREE_NAMESPACE };
22
22
  const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
23
23
  // ---------------------------------------------------------------------------
@@ -391,6 +391,138 @@ export function updatePrinciple(stateDir, principleId, updates) {
391
391
  return next;
392
392
  });
393
393
  }
394
+ // ---------------------------------------------------------------------------
395
+ // PRI-917 PR3B: reuse evidence append (SPEC v0.2.1 §12)
396
+ // ---------------------------------------------------------------------------
397
+ /**
398
+ * Thrown when the ledger file's content is not trustworthy for a read-modify-
399
+ * write: it parses to an empty principles tree while the file exists on disk
400
+ * (empty, corrupt, or truncated), or an entity's persisted reuseEvidence field
401
+ * has a malformed shape. Unlike a missing principle, writing over this state
402
+ * would DESTROY ledger content, so the write refuses instead of proceeding
403
+ * (rc-3).
404
+ */
405
+ export class LedgerIntegrityError extends Error {
406
+ filePath;
407
+ constructor(message, filePath) {
408
+ super(message);
409
+ this.name = 'LedgerIntegrityError';
410
+ this.filePath = filePath;
411
+ }
412
+ }
413
+ const REUSE_EVIDENCE_KEYS = [
414
+ 'painId',
415
+ 'candidateId',
416
+ 'decision',
417
+ 'actor',
418
+ 'reason',
419
+ 'decidedAt',
420
+ 'decisionId',
421
+ ];
422
+ /**
423
+ * Runtime shape check for an untrusted reuse evidence entry (rc-1/rc-2/rc-3).
424
+ * The caller-side decision surface parses owner/model output, so the ledger
425
+ * writer re-validates at this boundary: any malformed or unexpected field
426
+ * refuses the write — a half-valid evidence row must never land in the ledger.
427
+ */
428
+ function validateReuseEvidenceEntry(entry) {
429
+ const refuse = (detail) => {
430
+ throw new Error(`appendReuseEvidence: malformed reuse evidence entry (${detail}) — refusing to write (fail closed).`);
431
+ };
432
+ if (!isRecord(entry))
433
+ refuse(`expected an object, got ${typeof entry}`);
434
+ for (const key of Object.keys(entry)) {
435
+ if (!REUSE_EVIDENCE_KEYS.includes(key))
436
+ refuse(`unknown field "${key}"`);
437
+ }
438
+ if (typeof entry.painId !== 'string' || entry.painId.trim() === '')
439
+ refuse('painId must be a non-empty string');
440
+ if (typeof entry.candidateId !== 'string' || entry.candidateId.trim() === '') {
441
+ refuse('candidateId must be a non-empty string');
442
+ }
443
+ if (entry.decision !== 'reuse')
444
+ refuse('decision must be exactly "reuse"');
445
+ if (!isRecord(entry.actor))
446
+ refuse('actor must be an object');
447
+ if (entry.actor.kind !== 'owner' && entry.actor.kind !== 'ai_owner') {
448
+ refuse('actor.kind must be "owner" or "ai_owner"');
449
+ }
450
+ if (typeof entry.actor.id !== 'string' || entry.actor.id.trim() === '')
451
+ refuse('actor.id must be a non-empty string');
452
+ if (typeof entry.reason !== 'string' || entry.reason.trim() === '')
453
+ refuse('reason must be a non-empty string');
454
+ if (typeof entry.decidedAt !== 'string' || entry.decidedAt.trim() === '' || Number.isNaN(Date.parse(entry.decidedAt))) {
455
+ refuse('decidedAt must be a parseable date string');
456
+ }
457
+ if (entry.decisionId !== undefined && (typeof entry.decisionId !== 'string' || entry.decisionId.trim() === '')) {
458
+ refuse('decisionId, when present, must be a non-empty string');
459
+ }
460
+ }
461
+ /**
462
+ * Append one reuse-evidence entry to an existing Principle (PRI-917 PR3B,
463
+ * SPEC v0.2.1 §12). The ONLY writer for `Principle.reuseEvidence`.
464
+ *
465
+ * Runs entirely inside the single-writer file lock via mutateLedger —
466
+ * read → integrity guard → dedupe → append → save — so a concurrent append
467
+ * can never be lost, and there is no load-then-update window between
468
+ * processes. Do NOT implement appends via addPrincipleToLedger (it replaces
469
+ * the whole entry and would silently drop existing evidence) or via an
470
+ * external loadLedger+updatePrinciple sequence (read outside the lock).
471
+ *
472
+ * Fail-closed:
473
+ * - a ledger file that exists but parses to an empty principles tree (empty
474
+ * file, corrupt, or truncated) is treated as corruption — the write
475
+ * refuses rather than risk replacing the corpus with the new entry alone
476
+ * (LedgerIntegrityError);
477
+ * - a missing principleId refuses the write (no file mutation);
478
+ * - a malformed entry refuses the write.
479
+ *
480
+ * Idempotency key: (principleId, entry.candidateId). A candidate resolves
481
+ * into a principle at most once, so replaying the same decision is a no-op
482
+ * that returns the existing evidence with `appended: false` (INV-R07). A
483
+ * no-op still goes through the normal save path, so the tree's lastUpdated
484
+ * is refreshed; the ledger content itself is unchanged.
485
+ *
486
+ * Append-only (INV-R05): no other Principle field — including updatedAt —
487
+ * is touched, and existing entries are never rewritten or removed. Target
488
+ * eligibility (e.g. archived/deprecated exclusion, SPEC §15) is the
489
+ * retrieval/proposal layer's invariant; this writer records the decision
490
+ * verbatim.
491
+ */
492
+ export function appendReuseEvidence(stateDir, principleId, entry) {
493
+ validateReuseEvidenceEntry(entry);
494
+ const filePath = getLedgerFilePath(stateDir);
495
+ // Stat captured just before the lock as a hint; the authoritative check is
496
+ // the in-lock one below against the parsed store. A file created between
497
+ // this stat and the lock cannot trip the guard (it only fires when the
498
+ // file already existed at stat time AND the parsed tree is empty).
499
+ const statBefore = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
500
+ return mutateLedger(stateDir, (store) => {
501
+ if (statBefore !== null && Object.keys(store.tree.principles).length === 0) {
502
+ throw new LedgerIntegrityError(`Ledger file ${filePath} exists (${statBefore.size} bytes) but parsed to an empty principles tree — ` +
503
+ 'the file is likely corrupt or truncated. Refusing to append reuse evidence, because the save ' +
504
+ 'below would replace the ledger with this write alone. Restore the ledger file first (fail closed).', filePath);
505
+ }
506
+ const principle = store.tree.principles[principleId];
507
+ if (!principle) {
508
+ throw new Error(`Cannot append reuse evidence: principle "${principleId}" does not exist in the ledger.`);
509
+ }
510
+ const existing = principle.reuseEvidence;
511
+ if (existing !== undefined && !Array.isArray(existing)) {
512
+ throw new LedgerIntegrityError(`Principle "${principleId}" has a malformed reuseEvidence field (expected an array, got ${typeof existing}). ` +
513
+ 'Refusing to append — fix or restore the ledger entry first (fail closed).', filePath);
514
+ }
515
+ const current = existing ?? [];
516
+ if (current.some((e) => isRecord(e) && e.candidateId === entry.candidateId)) {
517
+ // Idempotent replay (INV-R07): this candidate already resolved into
518
+ // this principle. No new entry, no new logical decision.
519
+ return { principleId, reuseEvidence: current, appended: false };
520
+ }
521
+ const next = [...current, entry];
522
+ principle.reuseEvidence = next;
523
+ return { principleId, reuseEvidence: next, appended: true };
524
+ });
525
+ }
394
526
  export function updatePrincipleValueMetrics(stateDir, principleId, metrics) {
395
527
  return mutateLedger(stateDir, (store) => {
396
528
  const next = { ...metrics, principleId };