principles-disciple 1.11.0 → 1.13.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
deleteImplementationAssetDir,
|
|
7
|
+
getImplementationAssetRoot,
|
|
8
|
+
loadManifest,
|
|
9
|
+
writeManifest,
|
|
10
|
+
loadEntrySource,
|
|
11
|
+
createImplementationAssetDir,
|
|
12
|
+
writeEntrySource,
|
|
13
|
+
type CodeImplementationManifest,
|
|
14
|
+
} from '../../src/core/code-implementation-storage.js';
|
|
15
|
+
import { safeRmDir } from '../test-utils.js';
|
|
16
|
+
|
|
17
|
+
describe('CodeImplementationStorage', () => {
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
let stateDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-cis-'));
|
|
23
|
+
stateDir = path.join(tempDir, '.state');
|
|
24
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
safeRmDir(tempDir);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
// getImplementationAssetRoot
|
|
33
|
+
// -------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe('getImplementationAssetRoot', () => {
|
|
36
|
+
it('returns correct path under .state/principles/implementations/{implId}', () => {
|
|
37
|
+
const result = getImplementationAssetRoot(stateDir, 'IMPL_001');
|
|
38
|
+
expect(result).toBe(path.join(stateDir, 'principles', 'implementations', 'IMPL_001'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws on implId with forward slash', () => {
|
|
42
|
+
expect(() => getImplementationAssetRoot(stateDir, 'foo/bar')).toThrow(
|
|
43
|
+
'Invalid implementation ID',
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws on implId with backslash', () => {
|
|
48
|
+
expect(() => getImplementationAssetRoot(stateDir, 'foo\\bar')).toThrow(
|
|
49
|
+
'Invalid implementation ID',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws on implId with double-dot traversal', () => {
|
|
54
|
+
expect(() => getImplementationAssetRoot(stateDir, '..')).toThrow(
|
|
55
|
+
'Invalid implementation ID',
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws on empty implId', () => {
|
|
60
|
+
expect(() => getImplementationAssetRoot(stateDir, '')).toThrow(
|
|
61
|
+
'Implementation ID must not be empty',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
// loadManifest
|
|
68
|
+
// -------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('loadManifest', () => {
|
|
71
|
+
it('returns null when no manifest file exists', () => {
|
|
72
|
+
const result = loadManifest(stateDir, 'nonexistent');
|
|
73
|
+
expect(result).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns parsed manifest when manifest file exists', () => {
|
|
77
|
+
const implId = 'IMPL_002';
|
|
78
|
+
const manifest: CodeImplementationManifest = {
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
entryFile: 'entry.js',
|
|
81
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
82
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
83
|
+
replaySampleRefs: [],
|
|
84
|
+
lastEvalReportRef: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Write directly (bypass writeManifest to test load independently)
|
|
88
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
89
|
+
fs.mkdirSync(assetRoot, { recursive: true });
|
|
90
|
+
fs.writeFileSync(path.join(assetRoot, 'manifest.json'), JSON.stringify(manifest), 'utf-8');
|
|
91
|
+
|
|
92
|
+
const result = loadManifest(stateDir, implId);
|
|
93
|
+
expect(result).toEqual(manifest);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns null for corrupted manifest JSON', () => {
|
|
97
|
+
const implId = 'IMPL_003';
|
|
98
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
99
|
+
fs.mkdirSync(assetRoot, { recursive: true });
|
|
100
|
+
fs.writeFileSync(path.join(assetRoot, 'manifest.json'), 'not-valid-json{{{', 'utf-8');
|
|
101
|
+
|
|
102
|
+
const result = loadManifest(stateDir, implId);
|
|
103
|
+
expect(result).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
// writeManifest
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe('writeManifest', () => {
|
|
112
|
+
it('creates manifest file with correct JSON content', () => {
|
|
113
|
+
const implId = 'IMPL_004';
|
|
114
|
+
const manifest: CodeImplementationManifest = {
|
|
115
|
+
version: '2.0.0',
|
|
116
|
+
entryFile: 'entry.js',
|
|
117
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
118
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
119
|
+
replaySampleRefs: ['fp-001', 'fp-002'],
|
|
120
|
+
lastEvalReportRef: 'replays/2026-01-02.json',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
writeManifest(stateDir, implId, manifest);
|
|
124
|
+
|
|
125
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
126
|
+
const manifestPath = path.join(assetRoot, 'manifest.json');
|
|
127
|
+
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
128
|
+
|
|
129
|
+
const written = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
130
|
+
expect(written).toEqual(manifest);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('creates asset directory if it does not exist', () => {
|
|
134
|
+
const implId = 'IMPL_005';
|
|
135
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
136
|
+
expect(fs.existsSync(assetRoot)).toBe(false);
|
|
137
|
+
|
|
138
|
+
writeManifest(stateDir, implId, {
|
|
139
|
+
version: '1.0.0',
|
|
140
|
+
entryFile: 'entry.js',
|
|
141
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
142
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
143
|
+
replaySampleRefs: [],
|
|
144
|
+
lastEvalReportRef: null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(fs.existsSync(assetRoot)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('uses withLock for atomic writes (manifest file is written atomically)', () => {
|
|
151
|
+
const implId = 'IMPL_006';
|
|
152
|
+
|
|
153
|
+
// Write twice rapidly - withLock should prevent corruption
|
|
154
|
+
const manifest1: CodeImplementationManifest = {
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
entryFile: 'entry.js',
|
|
157
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
158
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
159
|
+
replaySampleRefs: [],
|
|
160
|
+
lastEvalReportRef: null,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
writeManifest(stateDir, implId, manifest1);
|
|
164
|
+
|
|
165
|
+
const manifest2: CodeImplementationManifest = {
|
|
166
|
+
...manifest1,
|
|
167
|
+
version: '2.0.0',
|
|
168
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
writeManifest(stateDir, implId, manifest2);
|
|
172
|
+
|
|
173
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
174
|
+
const written = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
|
|
175
|
+
expect(written.version).toBe('2.0.0');
|
|
176
|
+
expect(written.updatedAt).toBe('2026-01-02T00:00:00.000Z');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
// loadEntrySource
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
describe('loadEntrySource', () => {
|
|
185
|
+
it('returns null when no manifest exists', () => {
|
|
186
|
+
const result = loadEntrySource(stateDir, 'nonexistent');
|
|
187
|
+
expect(result).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns null when manifest exists but entry file does not', () => {
|
|
191
|
+
const implId = 'IMPL_007';
|
|
192
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
193
|
+
fs.mkdirSync(assetRoot, { recursive: true });
|
|
194
|
+
|
|
195
|
+
// Write manifest pointing to a non-existent entry file
|
|
196
|
+
const manifest: CodeImplementationManifest = {
|
|
197
|
+
version: '1.0.0',
|
|
198
|
+
entryFile: 'nonexistent.js',
|
|
199
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
200
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
201
|
+
replaySampleRefs: [],
|
|
202
|
+
lastEvalReportRef: null,
|
|
203
|
+
};
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
path.join(assetRoot, 'manifest.json'),
|
|
206
|
+
JSON.stringify(manifest),
|
|
207
|
+
'utf-8',
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const result = loadEntrySource(stateDir, implId);
|
|
211
|
+
expect(result).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns file content when entry file exists', () => {
|
|
215
|
+
const implId = 'IMPL_008';
|
|
216
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
217
|
+
fs.mkdirSync(assetRoot, { recursive: true });
|
|
218
|
+
|
|
219
|
+
const entryContent = 'export function evaluate() { return true; }';
|
|
220
|
+
fs.writeFileSync(path.join(assetRoot, 'entry.js'), entryContent, 'utf-8');
|
|
221
|
+
|
|
222
|
+
const manifest: CodeImplementationManifest = {
|
|
223
|
+
version: '1.0.0',
|
|
224
|
+
entryFile: 'entry.js',
|
|
225
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
226
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
227
|
+
replaySampleRefs: [],
|
|
228
|
+
lastEvalReportRef: null,
|
|
229
|
+
};
|
|
230
|
+
fs.writeFileSync(
|
|
231
|
+
path.join(assetRoot, 'manifest.json'),
|
|
232
|
+
JSON.stringify(manifest),
|
|
233
|
+
'utf-8',
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const result = loadEntrySource(stateDir, implId);
|
|
237
|
+
expect(result).toBe(entryContent);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
// createImplementationAssetDir
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe('createImplementationAssetDir', () => {
|
|
246
|
+
it('creates directory structure with manifest.json, entry.js, replays/ dir', () => {
|
|
247
|
+
const implId = 'IMPL_009';
|
|
248
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
249
|
+
|
|
250
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
251
|
+
expect(fs.existsSync(path.join(assetRoot, 'manifest.json'))).toBe(true);
|
|
252
|
+
expect(fs.existsSync(path.join(assetRoot, 'entry.js'))).toBe(true);
|
|
253
|
+
expect(fs.existsSync(path.join(assetRoot, 'replays'))).toBe(true);
|
|
254
|
+
expect(fs.statSync(path.join(assetRoot, 'replays')).isDirectory()).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('manifest has correct fields', () => {
|
|
258
|
+
const implId = 'IMPL_010';
|
|
259
|
+
const manifest = createImplementationAssetDir(stateDir, implId, '3.5.0');
|
|
260
|
+
|
|
261
|
+
expect(manifest.version).toBe('3.5.0');
|
|
262
|
+
expect(manifest.entryFile).toBe('entry.js');
|
|
263
|
+
expect(typeof manifest.createdAt).toBe('string');
|
|
264
|
+
expect(typeof manifest.updatedAt).toBe('string');
|
|
265
|
+
expect(manifest.replaySampleRefs).toEqual([]);
|
|
266
|
+
expect(manifest.lastEvalReportRef).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('manifest does NOT contain lifecycleState field', () => {
|
|
270
|
+
const implId = 'IMPL_011';
|
|
271
|
+
const manifest = createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
272
|
+
|
|
273
|
+
expect((manifest as any).lifecycleState).toBeUndefined();
|
|
274
|
+
expect('lifecycleState' in manifest).toBe(false);
|
|
275
|
+
|
|
276
|
+
// Also verify persisted manifest lacks lifecycleState
|
|
277
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
278
|
+
const raw = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
|
|
279
|
+
expect('lifecycleState' in raw).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('persists lineage metadata when provided for a generated candidate', () => {
|
|
283
|
+
const implId = 'IMPL_011A';
|
|
284
|
+
const manifest = createImplementationAssetDir(stateDir, implId, '1.0.0', {
|
|
285
|
+
lineage: {
|
|
286
|
+
principleId: 'P-001',
|
|
287
|
+
ruleId: 'R-001',
|
|
288
|
+
sourceSnapshotRef: 'snapshot-session-1',
|
|
289
|
+
sourcePainIds: ['pain:gate:1'],
|
|
290
|
+
sourceGateBlockIds: ['gate:write:1'],
|
|
291
|
+
sourceSessionId: 'session-1',
|
|
292
|
+
artificerArtifactId: 'artifact-1',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(manifest.lineage).toMatchObject({
|
|
297
|
+
principleId: 'P-001',
|
|
298
|
+
ruleId: 'R-001',
|
|
299
|
+
sourceSessionId: 'session-1',
|
|
300
|
+
artificerArtifactId: 'artifact-1',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const loaded = loadManifest(stateDir, implId);
|
|
304
|
+
expect(loaded?.lineage).toMatchObject({
|
|
305
|
+
sourcePainIds: ['pain:gate:1'],
|
|
306
|
+
sourceGateBlockIds: ['gate:write:1'],
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('is idempotent: repeated calls do not overwrite existing entry.js', () => {
|
|
311
|
+
const implId = 'IMPL_012';
|
|
312
|
+
|
|
313
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
314
|
+
|
|
315
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
316
|
+
const entryPath = path.join(assetRoot, 'entry.js');
|
|
317
|
+
|
|
318
|
+
// Overwrite entry.js with custom content
|
|
319
|
+
const customContent = '// CUSTOM CONTENT - should survive';
|
|
320
|
+
fs.writeFileSync(entryPath, customContent, 'utf-8');
|
|
321
|
+
|
|
322
|
+
// Call again
|
|
323
|
+
createImplementationAssetDir(stateDir, implId, '2.0.0');
|
|
324
|
+
|
|
325
|
+
// Custom content should still be there
|
|
326
|
+
const content = fs.readFileSync(entryPath, 'utf-8');
|
|
327
|
+
expect(content).toBe(customContent);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('manifest path matches Phase 13 replay-engine path convention', () => {
|
|
331
|
+
const implId = 'IMPL_013';
|
|
332
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
333
|
+
|
|
334
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
335
|
+
|
|
336
|
+
// Phase 13 replay-engine persists reports under:
|
|
337
|
+
// {stateDir}/.state/principles/implementations/{implId}/replays/{timestamp}.json
|
|
338
|
+
const replaysDir = path.join(assetRoot, 'replays');
|
|
339
|
+
expect(fs.existsSync(replaysDir)).toBe(true);
|
|
340
|
+
|
|
341
|
+
// Verify the full convention path is constructable
|
|
342
|
+
const reportPath = path.join(replaysDir, '2026-01-01T00-00-00.000Z.json');
|
|
343
|
+
fs.writeFileSync(reportPath, '{"test": true}', 'utf-8');
|
|
344
|
+
expect(fs.existsSync(reportPath)).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('manifest is loadable via loadManifest after creation', () => {
|
|
348
|
+
const implId = 'IMPL_014';
|
|
349
|
+
createImplementationAssetDir(stateDir, implId, '4.0.0');
|
|
350
|
+
|
|
351
|
+
const loaded = loadManifest(stateDir, implId);
|
|
352
|
+
expect(loaded).not.toBeNull();
|
|
353
|
+
expect(loaded!.version).toBe('4.0.0');
|
|
354
|
+
expect(loaded!.entryFile).toBe('entry.js');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('entry source is loadable via loadEntrySource after creation', () => {
|
|
358
|
+
const implId = 'IMPL_015';
|
|
359
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
360
|
+
|
|
361
|
+
const source = loadEntrySource(stateDir, implId);
|
|
362
|
+
expect(source).not.toBeNull();
|
|
363
|
+
expect(source).toContain('export const meta');
|
|
364
|
+
expect(source).toContain('export function evaluate');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('writes generated entry source when provided', () => {
|
|
368
|
+
const implId = 'IMPL_016';
|
|
369
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0', {
|
|
370
|
+
entrySource: 'export const meta = { name: "x", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "ok" }; }',
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const source = loadEntrySource(stateDir, implId);
|
|
374
|
+
expect(source).toContain('export const meta');
|
|
375
|
+
expect(source).not.toContain('placeholder');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('entry and cleanup helpers', () => {
|
|
380
|
+
it('overwrites entry source through writeEntrySource', () => {
|
|
381
|
+
const implId = 'IMPL_017';
|
|
382
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
383
|
+
|
|
384
|
+
writeEntrySource(stateDir, implId, 'export const meta = { name: "updated", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "updated" }; }');
|
|
385
|
+
|
|
386
|
+
expect(loadEntrySource(stateDir, implId)).toContain('updated');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('removes the asset directory through deleteImplementationAssetDir', () => {
|
|
390
|
+
const implId = 'IMPL_018';
|
|
391
|
+
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
392
|
+
|
|
393
|
+
deleteImplementationAssetDir(stateDir, implId);
|
|
394
|
+
|
|
395
|
+
expect(fs.existsSync(getImplementationAssetRoot(stateDir, implId))).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -52,11 +52,12 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
const p = reducer.getPrincipleById(id!);
|
|
55
|
-
|
|
55
|
+
// #212: Default is weak_heuristic (not manual_only) — allows basic pattern-matching evaluation
|
|
56
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
56
57
|
expect(p?.detectorMetadata).toBeUndefined();
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
it('
|
|
60
|
+
it('downgrades deterministic to weak_heuristic when no detectorMetadata provided', () => {
|
|
60
61
|
const workspace = makeTempDir();
|
|
61
62
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
62
63
|
|
|
@@ -70,9 +71,8 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
const p = reducer.getPrincipleById(id!);
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
expect(p?.evaluability).toBe('manual_only');
|
|
74
|
+
// #212: deterministic without detectorMetadata → downgraded to weak_heuristic (not manual_only)
|
|
75
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
76
76
|
expect(p?.detectorMetadata).toBeUndefined();
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -90,9 +90,8 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
const p = reducer.getPrincipleById(id!);
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
expect(p?.evaluability).toBe('manual_only');
|
|
93
|
+
// #212: weak_heuristic does not require detectorMetadata — no downgrade
|
|
94
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
it('stores detectorMetadata when provided', () => {
|
|
@@ -131,14 +130,14 @@ describe('createPrincipleFromDiagnosis — evaluability defaults', () => {
|
|
|
131
130
|
triggerPattern: 'file write fails',
|
|
132
131
|
action: 'check permissions first',
|
|
133
132
|
source: 'diagnostician',
|
|
134
|
-
// evaluability omitted — defaults to
|
|
133
|
+
// evaluability omitted — defaults to weak_heuristic (#212)
|
|
135
134
|
detectorMetadata: VALID_DETECTOR_METADATA,
|
|
136
135
|
});
|
|
137
136
|
|
|
138
137
|
const p = reducer.getPrincipleById(id!);
|
|
139
|
-
//
|
|
140
|
-
expect(p?.evaluability).toBe('
|
|
141
|
-
expect(p?.detectorMetadata).toEqual(VALID_DETECTOR_METADATA);
|
|
138
|
+
// #212: Default is weak_heuristic — detectorMetadata stored and used
|
|
139
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
140
|
+
expect(p?.detectorMetadata).toEqual(VALID_DETECTOR_METADATA);
|
|
142
141
|
});
|
|
143
142
|
});
|
|
144
143
|
|
|
@@ -334,14 +333,14 @@ describe('candidate_created event — evaluability in event data', () => {
|
|
|
334
333
|
triggerPattern: 'file write fails',
|
|
335
334
|
action: 'check permissions',
|
|
336
335
|
source: 'diagnostician',
|
|
337
|
-
// no evaluability
|
|
336
|
+
// no evaluability — defaults to weak_heuristic (#212)
|
|
338
337
|
});
|
|
339
338
|
|
|
340
339
|
const events = reducer.getEventLog();
|
|
341
340
|
const candidateEvent = events.find(e => e.type === 'candidate_created');
|
|
342
341
|
expect(candidateEvent).toBeDefined();
|
|
343
|
-
// Defaults to '
|
|
344
|
-
expect((candidateEvent!.data as any).evaluability).toBe('
|
|
342
|
+
// #212: Defaults to 'weak_heuristic' (not manual_only)
|
|
343
|
+
expect((candidateEvent!.data as any).evaluability).toBe('weak_heuristic');
|
|
345
344
|
});
|
|
346
345
|
|
|
347
346
|
it('emits candidate_created event with detectorMetadata when provided', () => {
|
|
@@ -370,7 +369,7 @@ describe('candidate_created event — evaluability in event data', () => {
|
|
|
370
369
|
// ---------------------------------------------------------------------------
|
|
371
370
|
|
|
372
371
|
describe('manual_only classification — no automatic targeting', () => {
|
|
373
|
-
it('principle without detectorMetadata has
|
|
372
|
+
it('principle without detectorMetadata has weak_heuristic evaluability', () => {
|
|
374
373
|
const workspace = makeTempDir();
|
|
375
374
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
376
375
|
|
|
@@ -380,12 +379,13 @@ describe('manual_only classification — no automatic targeting', () => {
|
|
|
380
379
|
triggerPattern: 'generic error',
|
|
381
380
|
action: 'be careful',
|
|
382
381
|
source: 'diagnostician',
|
|
383
|
-
// no evaluability, no detectorMetadata
|
|
382
|
+
// no evaluability, no detectorMetadata — defaults to weak_heuristic (#212)
|
|
384
383
|
});
|
|
385
384
|
|
|
386
385
|
// Principle auto-promotes to probation (not active)
|
|
387
386
|
const p = reducer.getProbationPrinciples()[0];
|
|
388
|
-
|
|
387
|
+
// #212: Default is weak_heuristic (not manual_only)
|
|
388
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
389
389
|
expect(p?.detectorMetadata).toBeUndefined();
|
|
390
390
|
});
|
|
391
391
|
|
|
@@ -463,7 +463,7 @@ describe('getActivePrinciples / getProbationPrinciples — evaluability field',
|
|
|
463
463
|
// ---------------------------------------------------------------------------
|
|
464
464
|
|
|
465
465
|
describe('Malformed detectorMetadata — defense in depth', () => {
|
|
466
|
-
it('downgrades to
|
|
466
|
+
it('downgrades to weak_heuristic when confidence is not a valid enum value', () => {
|
|
467
467
|
const workspace = makeTempDir();
|
|
468
468
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
469
469
|
|
|
@@ -485,12 +485,11 @@ describe('Malformed detectorMetadata — defense in depth', () => {
|
|
|
485
485
|
});
|
|
486
486
|
|
|
487
487
|
const p = reducer.getPrincipleById(id!);
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
expect(p?.evaluability).toBe('manual_only');
|
|
488
|
+
// #212: deterministic with invalid metadata → downgraded to weak_heuristic (not manual_only)
|
|
489
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
491
490
|
});
|
|
492
491
|
|
|
493
|
-
it('
|
|
492
|
+
it('weak_heuristic accepts malformed metadata without downgrade', () => {
|
|
494
493
|
const workspace = makeTempDir();
|
|
495
494
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
496
495
|
|
|
@@ -511,10 +510,11 @@ describe('Malformed detectorMetadata — defense in depth', () => {
|
|
|
511
510
|
});
|
|
512
511
|
|
|
513
512
|
const p = reducer.getPrincipleById(id!);
|
|
514
|
-
|
|
513
|
+
// #212: weak_heuristic does not require detectorMetadata — no downgrade needed
|
|
514
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
515
515
|
});
|
|
516
516
|
|
|
517
|
-
it('
|
|
517
|
+
it('weak_heuristic accepts empty positiveSignals without downgrade', () => {
|
|
518
518
|
const workspace = makeTempDir();
|
|
519
519
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
520
520
|
|
|
@@ -535,10 +535,10 @@ describe('Malformed detectorMetadata — defense in depth', () => {
|
|
|
535
535
|
});
|
|
536
536
|
|
|
537
537
|
const p = reducer.getPrincipleById(id!);
|
|
538
|
-
expect(p?.evaluability).toBe('
|
|
538
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
539
539
|
});
|
|
540
540
|
|
|
541
|
-
it('
|
|
541
|
+
it('weak_heuristic accepts empty negativeSignals without downgrade', () => {
|
|
542
542
|
const workspace = makeTempDir();
|
|
543
543
|
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
544
544
|
|
|
@@ -559,7 +559,7 @@ describe('Malformed detectorMetadata — defense in depth', () => {
|
|
|
559
559
|
});
|
|
560
560
|
|
|
561
561
|
const p = reducer.getPrincipleById(id!);
|
|
562
|
-
expect(p?.evaluability).toBe('
|
|
562
|
+
expect(p?.evaluability).toBe('weak_heuristic');
|
|
563
563
|
});
|
|
564
564
|
|
|
565
565
|
it('accepts valid detectorMetadata with all three signal arrays non-empty', () => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
appendCandidateArtifactLineageRecord,
|
|
7
|
+
listArtifactLineageRecords,
|
|
8
|
+
} from '../../src/core/nocturnal-artifact-lineage.js';
|
|
9
|
+
import { safeRmDir } from '../test-utils.js';
|
|
10
|
+
|
|
11
|
+
describe('nocturnal-artifact-lineage', () => {
|
|
12
|
+
let workspaceDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-lineage-'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
safeRmDir(workspaceDir);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('appends a candidate lineage record with explicit pain and gate refs', () => {
|
|
23
|
+
appendCandidateArtifactLineageRecord(workspaceDir, {
|
|
24
|
+
artifactId: 'artifact-1',
|
|
25
|
+
principleId: 'P-001',
|
|
26
|
+
ruleId: 'R-001',
|
|
27
|
+
sessionId: 'session-1',
|
|
28
|
+
sourceSnapshotRef: 'snapshot-1',
|
|
29
|
+
sourcePainIds: ['pain:gate:1', 'pain:tool:2'],
|
|
30
|
+
sourceGateBlockIds: ['gate:write:1'],
|
|
31
|
+
storagePath: path.join(workspaceDir, '.state', 'principles', 'implementations', 'IMPL-1'),
|
|
32
|
+
implementationId: 'IMPL-1',
|
|
33
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const records = listArtifactLineageRecords(
|
|
37
|
+
workspaceDir,
|
|
38
|
+
'rule-implementation-candidate'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(records).toHaveLength(1);
|
|
42
|
+
expect(records[0]).toMatchObject({
|
|
43
|
+
artifactKind: 'rule-implementation-candidate',
|
|
44
|
+
artifactId: 'artifact-1',
|
|
45
|
+
principleId: 'P-001',
|
|
46
|
+
ruleId: 'R-001',
|
|
47
|
+
sourceSnapshotRef: 'snapshot-1',
|
|
48
|
+
sourcePainIds: ['pain:gate:1', 'pain:tool:2'],
|
|
49
|
+
sourceGateBlockIds: ['gate:write:1'],
|
|
50
|
+
implementationId: 'IMPL-1',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|