principles-disciple 1.159.0 → 1.161.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/dist/core/thinking-models.d.ts +13 -44
- package/dist/core/thinking-models.js +19 -206
- package/dist/index.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/core/surface-guard.d.ts +0 -31
- package/dist/core/surface-guard.js +0 -131
|
@@ -1,49 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thinking Models —
|
|
2
|
+
* Thinking Models — Plugin I/O Adapter (Stage 3 slim)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Pure logic (BUILTIN_PATTERNS, BUILTIN_PATTERN_MAP, getFallbackName,
|
|
5
|
+
* getFallbackDescription, deriveThinkingScenarios, types) lives in
|
|
6
|
+
* @principles/core/runtime-v2/thinking-models.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* This file owns only the I/O boundary: reading THINKING_OS.md from a
|
|
9
|
+
* workspace dir and merging it with the builtin patterns. It re-exports
|
|
10
|
+
* the pure functions and types so existing relative imports
|
|
11
|
+
* (../core/thinking-models.js) continue to resolve.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
antiPattern?: string;
|
|
18
|
-
patterns: RegExp[];
|
|
19
|
-
baselineScenarios: string[];
|
|
20
|
-
}
|
|
21
|
-
interface ThinkingModelMatch {
|
|
22
|
-
modelId: string;
|
|
23
|
-
matchedPattern: string;
|
|
24
|
-
}
|
|
25
|
-
interface ThinkingScenarioContext {
|
|
26
|
-
recentToolCalls?: {
|
|
27
|
-
toolName: string;
|
|
28
|
-
outcome: 'success' | 'failure' | 'blocked';
|
|
29
|
-
errorType?: string | null;
|
|
30
|
-
}[];
|
|
31
|
-
recentPainEvents?: {
|
|
32
|
-
source: string;
|
|
33
|
-
score: number;
|
|
34
|
-
}[];
|
|
35
|
-
recentGateBlocks?: {
|
|
36
|
-
toolName: string;
|
|
37
|
-
reason: string;
|
|
38
|
-
}[];
|
|
39
|
-
recentUserCorrections?: {
|
|
40
|
-
correctionCue?: string | null;
|
|
41
|
-
}[];
|
|
42
|
-
recentPrincipleEvents?: {
|
|
43
|
-
eventType: string;
|
|
44
|
-
principleId?: string | null;
|
|
45
|
-
}[];
|
|
46
|
-
}
|
|
13
|
+
import { type ThinkingModelDefinition, type ThinkingModelMatch } from '@principles/core/runtime-v2';
|
|
14
|
+
export { BUILTIN_PATTERN_MAP, getFallbackName, getFallbackDescription, } from '@principles/core/runtime-v2';
|
|
15
|
+
export { deriveThinkingScenarios } from '@principles/core/runtime-v2';
|
|
16
|
+
export type { ThinkingModelDefinition, ThinkingModelMatch, ThinkingScenarioContext, } from '@principles/core/runtime-v2';
|
|
47
17
|
/**
|
|
48
18
|
* Load thinking model definitions dynamically from THINKING_OS.md.
|
|
49
19
|
* Falls back to built-in definitions if parsing fails.
|
|
@@ -51,6 +21,5 @@ interface ThinkingScenarioContext {
|
|
|
51
21
|
* @param workspaceDir Optional. If provided, loads from that workspace's THINKING_OS.md.
|
|
52
22
|
*/
|
|
53
23
|
export declare function listThinkingModels(workspaceDir?: string): ThinkingModelDefinition[];
|
|
24
|
+
export declare function getThinkingModel(modelId: string, workspaceDir?: string): ThinkingModelDefinition | undefined;
|
|
54
25
|
export declare function detectThinkingModelMatches(text: string, workspaceDir?: string): ThinkingModelMatch[];
|
|
55
|
-
export declare function deriveThinkingScenarios(modelId: string, context: ThinkingScenarioContext): string[];
|
|
56
|
-
export {};
|
|
@@ -1,163 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thinking Models —
|
|
2
|
+
* Thinking Models — Plugin I/O Adapter (Stage 3 slim)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Pure logic (BUILTIN_PATTERNS, BUILTIN_PATTERN_MAP, getFallbackName,
|
|
5
|
+
* getFallbackDescription, deriveThinkingScenarios, types) lives in
|
|
6
|
+
* @principles/core/runtime-v2/thinking-models.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* This file owns only the I/O boundary: reading THINKING_OS.md from a
|
|
9
|
+
* workspace dir and merging it with the builtin patterns. It re-exports
|
|
10
|
+
* the pure functions and types so existing relative imports
|
|
11
|
+
* (../core/thinking-models.js) continue to resolve.
|
|
12
12
|
*/
|
|
13
13
|
import { loadThinkingOsFromWorkspace, generateDetectionPatterns } from './thinking-os-parser.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
patterns: [
|
|
19
|
-
// Thinking process language (AI reasoning output)
|
|
20
|
-
/let me (first )?(understand|map|outline|survey|review the (structure|architecture|dependencies))/i,
|
|
21
|
-
/before (changing|editing|touching) anything/i,
|
|
22
|
-
/让我先(梳理|理解|看看|盘点).*(结构|架构|依赖|全貌)/i,
|
|
23
|
-
/在执行任何.*/i,
|
|
24
|
-
// Decision directive language (nocturnal artifact betterDecision)
|
|
25
|
-
/review (the |)(docs|architecture|structure|requirements|constraints|dependencies|authorization)/i,
|
|
26
|
-
/read (the |)(docs|architecture|requirements|constraints|logs?|error|stack)/i,
|
|
27
|
-
/check (the |)(docs|architecture|source|authorization|requirements|error)/i,
|
|
28
|
-
/understand (the |)(structure|architecture|requirements|constraints|full (context|picture))/i,
|
|
29
|
-
/first (to|and|,) (verify|check|review|read|understand|confirm|diagnose)/i,
|
|
30
|
-
/(verify|check|review|read|confirm) .+ first/i,
|
|
31
|
-
],
|
|
32
|
-
baselineScenarios: ['exploration', 'discovery'],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: 'T-02',
|
|
36
|
-
patterns: [
|
|
37
|
-
// Thinking process language
|
|
38
|
-
/(type|test|contract|schema|interface) (constraint|requirement|check|validation)/i,
|
|
39
|
-
/(必须|需要).*(遵守|符合|满足).*(类型|测试|契约|接口|规范)/i,
|
|
40
|
-
// Decision directive language
|
|
41
|
-
/(trust|follow|respect|adhere to|meet) (the |)(constraint|requirement|contract|interface|specification|schema|type)/i,
|
|
42
|
-
/(check|verify|validate|ensure) (the |)(constraint|requirement|contract|interface|specification|type|schema)/i,
|
|
43
|
-
],
|
|
44
|
-
baselineScenarios: ['constraint-check', 'contract-verification'],
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: 'T-03',
|
|
48
|
-
patterns: [
|
|
49
|
-
// Thinking process language
|
|
50
|
-
/based on (the |this )?(evidence|logs?|output|error|stack trace|test result)/i,
|
|
51
|
-
/let me (check|verify|confirm|read|look at) (the |)(actual|source|code|file|log)/i,
|
|
52
|
-
/根据(日志|证据|输出|报错|堆栈|测试结果)/i,
|
|
53
|
-
// Decision directive language (evidence-based decision)
|
|
54
|
-
/(verify|check|confirm) (the |)(error|error code|error message|actual|evidence|logs?|output|stack)/i,
|
|
55
|
-
/(error|failure) (logs?|output|stack trace|message|code) (before|then)/i,
|
|
56
|
-
/(look at|read) (the |)(error|logs?|output|stack|actual|evidence|file|code)/i,
|
|
57
|
-
],
|
|
58
|
-
baselineScenarios: ['evidence-gathering', 'verification'],
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'T-04',
|
|
62
|
-
patterns: [
|
|
63
|
-
// Thinking process language
|
|
64
|
-
/this (is|would be) (irreversible|destructive|permanent|not easily undone)/i,
|
|
65
|
-
/(reversible|can be undone|safely roll back)/i,
|
|
66
|
-
/(不可逆|破坏性|无法回滚|可以回滚|安全撤销)/i,
|
|
67
|
-
// Decision directive language
|
|
68
|
-
/(irreversible|destructive|permanent|high impact) (action|change|operation)/i,
|
|
69
|
-
/(must be|need to be|should be) (reviewed|confirmed|verified|checked) (first|before)/i,
|
|
70
|
-
/(get|obtain|ask for) (confirmation|authorization|approval|permission) (first|before)/i,
|
|
71
|
-
],
|
|
72
|
-
baselineScenarios: ['risk-management', 'reversibility'],
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: 'T-05',
|
|
76
|
-
patterns: [
|
|
77
|
-
// Thinking process language
|
|
78
|
-
/we (must|should) (not|never|avoid|prevent|ensure we don't)/i,
|
|
79
|
-
/(critical|important) (not to|that we don't|to avoid)/i,
|
|
80
|
-
/(绝不能|必须避免|不可|禁止|确保不会)/i,
|
|
81
|
-
// Decision directive language
|
|
82
|
-
/(must|should|need to) (not |never |avoid )?(check|verify|review|read|confirm|ask)/i,
|
|
83
|
-
/(safety|safe) (check|gate|checklist|guard)/i,
|
|
84
|
-
],
|
|
85
|
-
baselineScenarios: ['guardrails', 'safety-rails'],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'T-06',
|
|
89
|
-
patterns: [
|
|
90
|
-
// Thinking process language
|
|
91
|
-
/(simpl(er|est|ify)|minimal|straightforward|lean) (approach|solution|fix|implementation)/i,
|
|
92
|
-
/(simple is better|keep it simple|no need to over)/i,
|
|
93
|
-
/(最简|更简单|精简|没必要过度设计)/i,
|
|
94
|
-
// Decision directive language
|
|
95
|
-
/(minimal|smallest|narrowest) (change|modification|impact|approach)/i,
|
|
96
|
-
/(only|just) (change|modify|touch|edit|affect) (the |what)/i,
|
|
97
|
-
],
|
|
98
|
-
baselineScenarios: ['simplification', 'pragmatism'],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: 'T-07',
|
|
102
|
-
patterns: [
|
|
103
|
-
// Thinking process language
|
|
104
|
-
/(minimal|smallest|narrowest|least) (change|diff|modification|impact)/i,
|
|
105
|
-
/only (change|modify|touch|edit) (the |what)/i,
|
|
106
|
-
/(最小改动|最小变更|只改|只动必要部分)/i,
|
|
107
|
-
// Decision directive language
|
|
108
|
-
/(limit|narrow) (the |)(change|impact|scope|blast radius)/i,
|
|
109
|
-
],
|
|
110
|
-
baselineScenarios: ['minimal-diff', 'blast-radius-control'],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: 'T-08',
|
|
114
|
-
patterns: [
|
|
115
|
-
// Thinking process language
|
|
116
|
-
/this (error|failure|issue) (tells us|indicates|signals|suggests|means)/i,
|
|
117
|
-
/let me (stop|pause|step back|reconsider|rethink)/i,
|
|
118
|
-
/这个(错误|失败|问题).*(说明|意味着|提示)/i,
|
|
119
|
-
/让我(停下|暂停|退一步|重新考虑|重新审视)/i,
|
|
120
|
-
// Decision directive language
|
|
121
|
-
/(diagnose|analyze|investigate) (the |)(root |)(cause|reason|error|failure) (first|before)/i,
|
|
122
|
-
/(understand|diagnose|analyze) (the |)(error|failure|issue|problem) (first|before)/i,
|
|
123
|
-
],
|
|
124
|
-
baselineScenarios: ['reflection', 'pain-response'],
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'T-09',
|
|
128
|
-
patterns: [
|
|
129
|
-
// Thinking process language
|
|
130
|
-
/(break|split|decompose|divide) (this |the task |it )?(into|down)/i,
|
|
131
|
-
/(step 1|first,? (we|i|let's)|phase 1)/i,
|
|
132
|
-
/(拆分|分解|分步|分阶段|第一步)/i,
|
|
133
|
-
// Decision directive language
|
|
134
|
-
/(break|split|decompose|divide) (the |)(task|problem|action) (into|into steps)/i,
|
|
135
|
-
],
|
|
136
|
-
baselineScenarios: ['decomposition', 'phased-execution'],
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 'T-10',
|
|
140
|
-
patterns: [
|
|
141
|
-
// Thinking process language
|
|
142
|
-
/let me (write|save|record|note down|document)/i,
|
|
143
|
-
/memory.*scratchpad|write.*plan\.md|write.*memory|memory.*persist/i,
|
|
144
|
-
/(让我.*写入|写入.*memory|记录.*scratchpad)/i,
|
|
145
|
-
// Decision directive language
|
|
146
|
-
/(write|save|record|note) (the |)(conclusion|decision|plan|next step) (to|in) (file|plan\.md|scratchpad)/i,
|
|
147
|
-
],
|
|
148
|
-
baselineScenarios: ['memory-persistence', 'state-externalization'],
|
|
149
|
-
},
|
|
150
|
-
];
|
|
151
|
-
const BUILTIN_PATTERN_MAP = new Map(BUILTIN_PATTERNS.map((p) => [p.id, p]));
|
|
152
|
-
// Fallback name/description lookup — delegates to Core Principle Registry
|
|
153
|
-
function getFallbackName(id) {
|
|
154
|
-
const entry = CORE_PRINCIPLES.find(p => p.id === id);
|
|
155
|
-
return entry?.name ?? id;
|
|
156
|
-
}
|
|
157
|
-
function getFallbackDescription(id) {
|
|
158
|
-
const entry = CORE_PRINCIPLES.find(p => p.id === id);
|
|
159
|
-
return entry?.statement ?? '';
|
|
160
|
-
}
|
|
14
|
+
import { BUILTIN_PATTERN_MAP, getFallbackName, getFallbackDescription, } from '@principles/core/runtime-v2';
|
|
15
|
+
// Re-export pure logic + types so existing imports from this module keep working.
|
|
16
|
+
export { BUILTIN_PATTERN_MAP, getFallbackName, getFallbackDescription, } from '@principles/core/runtime-v2';
|
|
17
|
+
export { deriveThinkingScenarios } from '@principles/core/runtime-v2';
|
|
161
18
|
// ---------------------------------------------------------------------------
|
|
162
19
|
// Runtime model definitions — merged from THINKING_OS.md + builtin patterns
|
|
163
20
|
// ---------------------------------------------------------------------------
|
|
@@ -200,20 +57,20 @@ export function listThinkingModels(workspaceDir) {
|
|
|
200
57
|
}
|
|
201
58
|
}
|
|
202
59
|
// Fallback: built-in definitions
|
|
203
|
-
for (const
|
|
60
|
+
for (const [id, builtin] of BUILTIN_PATTERN_MAP) {
|
|
204
61
|
models.push({
|
|
205
|
-
id
|
|
206
|
-
name: getFallbackName(
|
|
207
|
-
description: getFallbackDescription(
|
|
208
|
-
patterns:
|
|
209
|
-
baselineScenarios:
|
|
62
|
+
id,
|
|
63
|
+
name: getFallbackName(id),
|
|
64
|
+
description: getFallbackDescription(id),
|
|
65
|
+
patterns: builtin.patterns,
|
|
66
|
+
baselineScenarios: builtin.baselineScenarios,
|
|
210
67
|
});
|
|
211
68
|
}
|
|
212
69
|
_cachedDefinitions = models;
|
|
213
70
|
_cachedWorkspace = cacheKey;
|
|
214
71
|
return models.slice();
|
|
215
72
|
}
|
|
216
|
-
function getThinkingModel(modelId, workspaceDir) {
|
|
73
|
+
export function getThinkingModel(modelId, workspaceDir) {
|
|
217
74
|
const models = listThinkingModels(workspaceDir);
|
|
218
75
|
return models.find(m => m.id === modelId);
|
|
219
76
|
}
|
|
@@ -235,47 +92,3 @@ export function detectThinkingModelMatches(text, workspaceDir) {
|
|
|
235
92
|
}
|
|
236
93
|
return matches;
|
|
237
94
|
}
|
|
238
|
-
export function deriveThinkingScenarios(modelId, context) {
|
|
239
|
-
const scenarios = new Set(getThinkingModel(modelId)?.baselineScenarios ?? []);
|
|
240
|
-
if ((context.recentToolCalls ?? []).some((call) => call.outcome === 'failure')) {
|
|
241
|
-
scenarios.add('after-tool-failure');
|
|
242
|
-
}
|
|
243
|
-
// after-recovery: success that follows a failure (not just any success)
|
|
244
|
-
const calls = context.recentToolCalls ?? [];
|
|
245
|
-
const hasFailure = calls.some((call) => call.outcome === 'failure');
|
|
246
|
-
const hasSuccess = calls.some((call) => call.outcome === 'success');
|
|
247
|
-
if (hasFailure && hasSuccess) {
|
|
248
|
-
scenarios.add('after-recovery');
|
|
249
|
-
}
|
|
250
|
-
if ((context.recentToolCalls ?? []).some((call) => call.outcome === 'blocked')) {
|
|
251
|
-
scenarios.add('blocked-execution');
|
|
252
|
-
}
|
|
253
|
-
if ((context.recentToolCalls ?? []).some((call) => Boolean(call.errorType))) {
|
|
254
|
-
scenarios.add('incident-response');
|
|
255
|
-
}
|
|
256
|
-
if ((context.recentPainEvents ?? []).length > 0) {
|
|
257
|
-
scenarios.add('user-friction');
|
|
258
|
-
}
|
|
259
|
-
if ((context.recentGateBlocks ?? []).length > 0) {
|
|
260
|
-
scenarios.add('gate-block');
|
|
261
|
-
}
|
|
262
|
-
if ((context.recentUserCorrections ?? []).length > 0) {
|
|
263
|
-
scenarios.add('user-correction');
|
|
264
|
-
}
|
|
265
|
-
if ((context.recentPrincipleEvents ?? []).length > 0) {
|
|
266
|
-
scenarios.add('principle-feedback');
|
|
267
|
-
}
|
|
268
|
-
if (modelId === 'T-03') {
|
|
269
|
-
scenarios.add('root-cause-analysis');
|
|
270
|
-
}
|
|
271
|
-
if (modelId === 'T-04' || modelId === 'T-05') {
|
|
272
|
-
scenarios.add('risk-review');
|
|
273
|
-
}
|
|
274
|
-
if (modelId === 'T-08') {
|
|
275
|
-
scenarios.add('reflection-loop');
|
|
276
|
-
}
|
|
277
|
-
if (modelId === 'T-09') {
|
|
278
|
-
scenarios.add('task-planning');
|
|
279
|
-
}
|
|
280
|
-
return Array.from(scenarios);
|
|
281
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import { SystemLogger } from './core/system-logger.js';
|
|
|
38
38
|
import { PathResolver } from './core/path-resolver.js';
|
|
39
39
|
import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe, resolveHookWorkspaceDir } from './utils/workspace-resolver.js';
|
|
40
40
|
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
41
|
-
import { checkSurfaceGuard, guardHook, guardService } from '
|
|
41
|
+
import { checkSurfaceGuard, guardHook, guardService } from '@principles/core/runtime-v2';
|
|
42
42
|
// Track started workspaces — one-time init + evolution worker per workspace
|
|
43
43
|
const startedWorkspaces = new Set();
|
|
44
44
|
// ── Conversation Access Health Check (PRI-343) ────────────────────────────
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.161.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory, type PluginSurfaceEntry, type MvpCategory } from '@principles/core/runtime-v2';
|
|
2
|
-
import type { OpenClawPluginService } from '../openclaw-sdk.js';
|
|
3
|
-
export interface SurfaceGuardResult {
|
|
4
|
-
passed: boolean;
|
|
5
|
-
enabledCoreSurfaces: string[];
|
|
6
|
-
disabledNonCoreSurfaces: string[];
|
|
7
|
-
violations: string[];
|
|
8
|
-
warnings: string[];
|
|
9
|
-
}
|
|
10
|
-
type LoggerLike = {
|
|
11
|
-
info?: (msg: string) => void;
|
|
12
|
-
debug?: (msg: string) => void;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Reset the per-process surface-guard skip log bookkeeping. Intended for tests
|
|
16
|
-
* that need to assert on the first-fire log without cross-test pollution.
|
|
17
|
-
* Not part of the production API surface; do not call from runtime code.
|
|
18
|
-
*/
|
|
19
|
-
export declare function __resetSurfaceGuardSkipLogStateForTests(): void;
|
|
20
|
-
export declare function checkSurfaceGuard(): SurfaceGuardResult;
|
|
21
|
-
export declare function getSurfaceIdForHook(hookEvent: string, label?: string): string;
|
|
22
|
-
export declare function getSurfaceIdForService(serviceName: string): string;
|
|
23
|
-
export declare function isSurfaceEnabled(surfaceId: string, overrides?: Record<string, boolean>): {
|
|
24
|
-
enabled: boolean;
|
|
25
|
-
reason?: string;
|
|
26
|
-
};
|
|
27
|
-
export type HookHandler<E, C, R> = (event: E, ctx: C) => R | Promise<R>;
|
|
28
|
-
export declare function guardHook<E, C, R>(surfaceId: string, logger: LoggerLike | undefined, handler: HookHandler<E, C, R>): HookHandler<E, C, R>;
|
|
29
|
-
export declare function guardService<T extends OpenClawPluginService>(surfaceId: string, service: T, logger?: LoggerLike): T | null;
|
|
30
|
-
export { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory };
|
|
31
|
-
export type { PluginSurfaceEntry, MvpCategory };
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory, } from '@principles/core/runtime-v2';
|
|
2
|
-
// Surface-level once-only log state (PRI-298).
|
|
3
|
-
// The first time a quiet/non-core surface guard actually fires in this
|
|
4
|
-
// process, the disabled reason is emitted once. Subsequent fires for the
|
|
5
|
-
// same surfaceId are still observable (the no-op handler preserves
|
|
6
|
-
// behaviour) but no longer flood the log. Fresh processes start with an
|
|
7
|
-
// empty set, so each plugin load gets one observable skip per surface.
|
|
8
|
-
//
|
|
9
|
-
// The Set is only updated when the log was actually emitted, so passing
|
|
10
|
-
// `undefined` for the logger on a quiet first fire does NOT consume the
|
|
11
|
-
// one-shot slot — a later registration that supplies a logger still gets
|
|
12
|
-
// the first-fire reason (PRI-298 / ERR-002).
|
|
13
|
-
const loggedSkipSurfaces = new Set();
|
|
14
|
-
/**
|
|
15
|
-
* Emit the disabled-reason log line for `surfaceId` at most once per
|
|
16
|
-
* process. Returns true if the log was emitted, false if it was suppressed
|
|
17
|
-
* (already logged, or no logger available). Only marks the surface as
|
|
18
|
-
* logged when the log was actually written, so a missing logger on first
|
|
19
|
-
* call does not consume the one-shot slot.
|
|
20
|
-
*/
|
|
21
|
-
function logSkipOnce(surfaceId, logger, message) {
|
|
22
|
-
if (loggedSkipSurfaces.has(surfaceId)) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
if (!logger?.info) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
loggedSkipSurfaces.add(surfaceId);
|
|
29
|
-
logger.info(message);
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Reset the per-process surface-guard skip log bookkeeping. Intended for tests
|
|
34
|
-
* that need to assert on the first-fire log without cross-test pollution.
|
|
35
|
-
* Not part of the production API surface; do not call from runtime code.
|
|
36
|
-
*/
|
|
37
|
-
export function __resetSurfaceGuardSkipLogStateForTests() {
|
|
38
|
-
loggedSkipSurfaces.clear();
|
|
39
|
-
}
|
|
40
|
-
export function checkSurfaceGuard() {
|
|
41
|
-
const validation = validateSurfaceRegistry(PLUGIN_SURFACE_REGISTRY);
|
|
42
|
-
const violations = [];
|
|
43
|
-
const warnings = [...validation.warnings];
|
|
44
|
-
const coreSurfaces = getSurfacesByCategory(PLUGIN_SURFACE_REGISTRY, 'core');
|
|
45
|
-
const enabledCore = coreSurfaces.filter(s => s.enabledByDefault);
|
|
46
|
-
const nonCoreEnabled = PLUGIN_SURFACE_REGISTRY.filter(s => s.category !== 'core' && s.enabledByDefault);
|
|
47
|
-
if (nonCoreEnabled.length > 0) {
|
|
48
|
-
for (const surface of nonCoreEnabled) {
|
|
49
|
-
violations.push(`non-core surface '${surface.id}' (${surface.category}) is enabledByDefault=true — must be false per ADR-0014`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (!validation.valid) {
|
|
53
|
-
violations.push(...validation.errors);
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
passed: violations.length === 0,
|
|
57
|
-
enabledCoreSurfaces: enabledCore.map(s => s.id),
|
|
58
|
-
disabledNonCoreSurfaces: PLUGIN_SURFACE_REGISTRY
|
|
59
|
-
.filter(s => s.category !== 'core' && !s.enabledByDefault)
|
|
60
|
-
.map(s => s.id),
|
|
61
|
-
violations,
|
|
62
|
-
warnings,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function getSurfaceIdForHook(hookEvent, label) {
|
|
66
|
-
if (label) {
|
|
67
|
-
return `hook:${hookEvent}.${label}`;
|
|
68
|
-
}
|
|
69
|
-
return `hook:${hookEvent}`;
|
|
70
|
-
}
|
|
71
|
-
export function getSurfaceIdForService(serviceName) {
|
|
72
|
-
return `service:${serviceName}`;
|
|
73
|
-
}
|
|
74
|
-
export function isSurfaceEnabled(surfaceId, overrides = {}) {
|
|
75
|
-
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
|
|
76
|
-
if (!entry) {
|
|
77
|
-
return {
|
|
78
|
-
enabled: false,
|
|
79
|
-
reason: `surface '${surfaceId}' not found in registry — classify before enabling (PRI-289)`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (Object.hasOwn(overrides, surfaceId)) {
|
|
83
|
-
const override = overrides[surfaceId];
|
|
84
|
-
if (typeof override !== 'boolean') {
|
|
85
|
-
return { enabled: entry.enabledByDefault, reason: `override for '${surfaceId}' is not boolean, using default` };
|
|
86
|
-
}
|
|
87
|
-
if (entry.category === 'gone') {
|
|
88
|
-
return { enabled: false, reason: `surface '${surfaceId}' is gone and cannot be re-enabled` };
|
|
89
|
-
}
|
|
90
|
-
if (entry.category === 'core' && !override) {
|
|
91
|
-
return { enabled: true, reason: `surface '${surfaceId}' is core and cannot be disabled` };
|
|
92
|
-
}
|
|
93
|
-
return { enabled: override };
|
|
94
|
-
}
|
|
95
|
-
if (!entry.enabledByDefault && entry.disabledReason) {
|
|
96
|
-
return { enabled: false, reason: entry.disabledReason };
|
|
97
|
-
}
|
|
98
|
-
return { enabled: entry.enabledByDefault };
|
|
99
|
-
}
|
|
100
|
-
export function guardHook(surfaceId, logger, handler) {
|
|
101
|
-
const check = isSurfaceEnabled(surfaceId);
|
|
102
|
-
if (check.enabled) {
|
|
103
|
-
return handler;
|
|
104
|
-
}
|
|
105
|
-
const reason = check.reason ?? 'surface not enabled';
|
|
106
|
-
// Log on the first ACTUAL no-op invocation, not at construction time
|
|
107
|
-
// (PRI-298). Construction-time logging would emit a `SKIP` line at
|
|
108
|
-
// plugin startup for every registered quiet hook, regardless of
|
|
109
|
-
// whether the hook ever fires — which is exactly the startup log
|
|
110
|
-
// noise this change is meant to prevent. The one-shot is consumed only
|
|
111
|
-
// when the log was actually written, so `undefined` logger on first
|
|
112
|
-
// call does not eat the slot.
|
|
113
|
-
return (_event, _ctx) => {
|
|
114
|
-
logSkipOnce(surfaceId, logger, `[PD:surface-guard] SKIP ${surfaceId}: ${reason}`);
|
|
115
|
-
return undefined;
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
export function guardService(surfaceId, service, logger) {
|
|
119
|
-
const check = isSurfaceEnabled(surfaceId);
|
|
120
|
-
if (check.enabled) {
|
|
121
|
-
return service;
|
|
122
|
-
}
|
|
123
|
-
const reason = check.reason ?? 'surface not enabled';
|
|
124
|
-
// guardService is called once per service during plugin registration,
|
|
125
|
-
// so the once-only check fires on the registration call itself. The
|
|
126
|
-
// shared helper makes the consumption rule identical to guardHook:
|
|
127
|
-
// a missing logger on first call does not consume the one-shot.
|
|
128
|
-
logSkipOnce(surfaceId, logger, `[PD:surface-guard] SKIP service ${surfaceId}: ${reason}`);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
export { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory };
|