@arcfold/dsh 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/cordis.patch.yml +138 -0
- package/package.json +70 -4
- package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
- package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
- package/presets/arcfold.patch.yml +110 -0
- package/runtime/README.md +19 -0
- package/runtime/bootstrap.json +15 -0
- package/runtime/desktop-python.tar.gz +0 -0
- package/runtime/host/package.json +11 -0
- package/runtime/host/pnpm-workspace.yaml +9 -0
- package/runtime/initial-policy.json +94 -0
- package/runtime/profile/pnpm-workspace.yaml +7 -0
- package/scripts/exact-package-patch.mjs +106 -0
- package/scripts/patch-deepseek-transport.mjs +14 -0
- package/src/agent.mjs +56 -0
- package/src/browse-scope.mjs +14 -0
- package/src/budget.mjs +369 -0
- package/src/business.mjs +334 -0
- package/src/context-material.mjs +119 -0
- package/src/deepseek-budget-plugin.mjs +58 -0
- package/src/deepseek-provider.mjs +57 -0
- package/src/deepseek-receipt.mjs +97 -0
- package/src/deepseek-transport.mjs +97 -0
- package/src/desktop/adapter.mjs +16 -0
- package/src/desktop/bootstrap.mjs +84 -0
- package/src/desktop/configuration.mjs +117 -0
- package/src/desktop/provider.mjs +46 -0
- package/src/desktop/python-runtime.mjs +105 -0
- package/src/desktop/setup-service.mjs +81 -0
- package/src/desktop/vendor/LICENSE.deepseek +21 -0
- package/src/desktop/vendor/NOTICE.md +5 -0
- package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
- package/src/evidence-ownership.mjs +51 -0
- package/src/evidence.mjs +35 -0
- package/src/goal-context.mjs +52 -0
- package/src/goal-execution-tool.mjs +74 -0
- package/src/goal-execution.mjs +333 -0
- package/src/goal-tool.mjs +56 -0
- package/src/goal-work-control.mjs +63 -0
- package/src/goal-work.mjs +257 -0
- package/src/goals.mjs +578 -0
- package/src/importer.mjs +298 -0
- package/src/index.mjs +53 -0
- package/src/learning-contracts.mjs +220 -0
- package/src/learning-defaults.mjs +98 -0
- package/src/learning-goals.mjs +165 -0
- package/src/learning-plugin.mjs +28 -0
- package/src/learning-runner.mjs +275 -0
- package/src/learning.mjs +534 -0
- package/src/legacy-export.mjs +332 -0
- package/src/legacy-import-source.mjs +356 -0
- package/src/metered-provider-host.mjs +101 -0
- package/src/metered-transport.mjs +435 -0
- package/src/method-context.mjs +57 -0
- package/src/method-lifecycle.mjs +34 -0
- package/src/method-tool.mjs +61 -0
- package/src/methods.mjs +425 -0
- package/src/model-selection.mjs +41 -0
- package/src/native-context-call.mjs +88 -0
- package/src/native-continuation.mjs +217 -0
- package/src/native-request-authority.mjs +90 -0
- package/src/openai-models.mjs +4 -0
- package/src/openai-receipt.mjs +98 -0
- package/src/openai-transport.mjs +167 -0
- package/src/openai-usage.mjs +194 -0
- package/src/opportunities.mjs +295 -0
- package/src/preparation-artifacts.mjs +213 -0
- package/src/preparation-plugin.mjs +290 -0
- package/src/runtime-command.mjs +264 -0
- package/src/runtime-config.mjs +417 -0
- package/src/runtime-guard.mjs +284 -0
- package/src/runtime-host.mjs +212 -0
- package/src/runtime-init.mjs +278 -0
- package/src/runtime-installation.mjs +518 -0
- package/src/runtime-lifecycle.mjs +227 -0
- package/src/runtime-model-settings.mjs +111 -0
- package/src/runtime-readiness.mjs +285 -0
- package/src/runtime-workspace.mjs +18 -0
- package/src/session-bridge.mjs +365 -0
- package/src/store.mjs +394 -0
- package/src/tree/LICENSE +21 -0
- package/src/tree/attribution.mjs +120 -0
- package/src/tree/clusters.mjs +61 -0
- package/src/tree/consolidation.mjs +103 -0
- package/src/tree/context.mjs +211 -0
- package/src/tree/contracts.mjs +142 -0
- package/src/tree/experience.mjs +140 -0
- package/src/tree/experiment-budget.mjs +84 -0
- package/src/tree/hooks.mjs +254 -0
- package/src/tree/host-context.mjs +107 -0
- package/src/tree/host-process.mjs +288 -0
- package/src/tree/host.mjs +37 -0
- package/src/tree/improvement.mjs +106 -0
- package/src/tree/items.mjs +294 -0
- package/src/tree/jobs.mjs +184 -0
- package/src/tree/learning.mjs +202 -0
- package/src/tree/metrics.mjs +203 -0
- package/src/tree/model-owner.mjs +254 -0
- package/src/tree/observations.mjs +73 -0
- package/src/tree/prompts.mjs +500 -0
- package/src/tree/reports.mjs +67 -0
- package/src/tree/repository.mjs +236 -0
- package/src/tree/runtime.mjs +745 -0
- package/src/tree/script-writer.mjs +149 -0
- package/src/tree/scripts.mjs +208 -0
- package/src/tree/signals.mjs +118 -0
- package/src/tree/snapshots.mjs +379 -0
- package/src/tree/token-counter.mjs +75 -0
- package/src/tree/token-counter.py +37 -0
- package/src/tree/tokenizer-resources/LICENSE +21 -0
- package/src/tree/tokenizer-resources/README.md +31 -0
- package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
- package/src/tree/tokenizer-resources/manifest.json +61 -0
- package/src/tree/tokenizer-resources/native.pyi +1042 -0
- package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
- package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
- package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
- package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
- package/src/tree/tools.mjs +99 -0
- package/src/tree/tree.mjs +202 -0
- package/src/tree/ui.mjs +285 -0
- package/src/tree/verification.mjs +143 -0
- package/src/understandings.mjs +309 -0
- package/src/web-budget-recovery.mjs +181 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { CONTRACT, canonicalJson, digest, fail, jsonClone, sourceRefs } from './contracts.mjs';
|
|
2
|
+
import { ItemService } from './items.mjs';
|
|
3
|
+
import { validateOutput, resolveSourceIds } from './prompts.mjs';
|
|
4
|
+
import { ScriptWriter } from './script-writer.mjs';
|
|
5
|
+
import { learningCorrections, LearningObservations, quotedSource } from './observations.mjs';
|
|
6
|
+
import { MetricsService } from './metrics.mjs';
|
|
7
|
+
|
|
8
|
+
const program = { kind: 'program', id: 'tree-learning' };
|
|
9
|
+
const unique = values => [...new Map(values.map(v => [canonicalJson(v), v])).values()];
|
|
10
|
+
export function itemInput({ item, version, method }) {
|
|
11
|
+
return { id: item.id, version: item.activeVersion, nodeId: item.nodeId, kind: item.kind, sourceKind: version.sourceKind, userProtected: item.userProtected,
|
|
12
|
+
originNodeId: item.originNodeId, originChain: item.originChain, content: version.content, method: method ?? null, validation: item.validation };
|
|
13
|
+
}
|
|
14
|
+
export function proposalInput(output, envelope) {
|
|
15
|
+
const refs = ids => resolveSourceIds(ids, envelope);
|
|
16
|
+
const method = proposal => {
|
|
17
|
+
const { sourceIds, origin: _origin, ...value } = proposal;
|
|
18
|
+
const category = envelope.trigger.directRequirement?.category;
|
|
19
|
+
return { ...value, expectedEffect: value.expectedEffect ?? { category, change: ['approved', 'adopted_proposal', 'confirmed_inference', 'proceeded'].includes(category) ? 'more' : 'less', text: `遵守本轮明确要求(${category})` }, sourceRefs: refs(sourceIds) };
|
|
20
|
+
};
|
|
21
|
+
return { nodeId: envelope.scope.nodeIds[0], understandings: (output.understandings ?? []).map(({ sourceIds, origin: _origin, ...value }) => ({ ...value, sourceRefs: refs(sourceIds) })),
|
|
22
|
+
methods: (output.methods ?? []).map(method), supplements: (output.supplements ?? []).map(({ sourceIds, ...value }) => {
|
|
23
|
+
const actual = refs(sourceIds); if (!actual.some(ref => envelope.sources.some(s => digest(s.ref) === digest(ref) && s.role === 'user' && s.origin === 'user'))) fail('SUPPLEMENT_USER_REQUIRED', 'Learning supplements need a real user requirement');
|
|
24
|
+
return { ...value, sourceRefs: actual, traitEvidence: actual, universalStopgap: false };
|
|
25
|
+
}), revisions: (output.revisions ?? []).map(({ sourceIds, correctionEvidence: _evidence, ...value }) => ({ ...value, sourceRefs: refs(sourceIds) })) };
|
|
26
|
+
}
|
|
27
|
+
export function itemServiceFor(repo, resolveSource, job, corrections) {
|
|
28
|
+
const correction = job.trigger.correction;
|
|
29
|
+
return new ItemService({ repository: repo, resolveSource, context: () => ({ actor: { kind: 'model', id: job.id, role: job.role }, nodeIds: job.nodeIds,
|
|
30
|
+
...(correction ? { correction: { ...correction, sourceRefs: resolveSourceIds(correction.sourceIds, job.input) } } : {}), ...(corrections ? { corrections } : {}) }) });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class LearningService {
|
|
34
|
+
constructor({ repository, experience, contextBuilder, consolidation, jobs, modelConfig, signals, writerRouter, annotations = () => ({}), outputProtocol = 'correction-v1' }) {
|
|
35
|
+
Object.assign(this, { repo: repository, experience, builder: contextBuilder, consolidation, jobs, config: modelConfig, signals, writers: writerRouter, annotations, outputProtocol });
|
|
36
|
+
this.resolveSource = contextBuilder.resolveSource;
|
|
37
|
+
this.observations = new LearningObservations({ repository, metrics: new MetricsService({ repository, experience, signals }) });
|
|
38
|
+
}
|
|
39
|
+
existing(turn, nodeId) {
|
|
40
|
+
return this.repo.list('tree_jobs', j => j.role === 'learning' && j.trigger.turnId === turn.id && j.nodeIds[0] === nodeId && j.input.currentTurn.assignmentRevision === turn.assignmentRevision)
|
|
41
|
+
.sort((a, b) => b.enqueuedOrder - a.enqueuedOrder || b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))[0];
|
|
42
|
+
}
|
|
43
|
+
enqueue(turnId, nodeId) {
|
|
44
|
+
const turn = this.repo.get('tree_turns', turnId);
|
|
45
|
+
if (!turn || turn.status !== 'settled' || !turn.nodeIds.includes(nodeId)) fail('LEARNING_EXPERIENCE', 'Learning needs a settled own main turn');
|
|
46
|
+
const baseKey = digest(['turn-learning', turnId, nodeId, turn.assignmentRevision]), old = this.existing(turn, nodeId);
|
|
47
|
+
const session = this.repo.list('tree_learning_sessions', s => s.nodeId === nodeId)[0];
|
|
48
|
+
// Only an append stopped before submission may be frozen again after a
|
|
49
|
+
// successful epoch change. Paid/unknown and ordinary failures never retry.
|
|
50
|
+
const deferred = old?.status === 'failed' && old.error?.code === 'LEARNING_PRESSURE' && !old.submittedAt && !old.invocationIds.length;
|
|
51
|
+
if (old && (!deferred || session?.paused || session?.contextEpoch <= old.contextEpoch)) return old;
|
|
52
|
+
const key = old ? digest(['epoch-learning', baseKey, session.contextEpoch]) : baseKey;
|
|
53
|
+
// Freeze after previous receipts, not eagerly while an earlier turn is still
|
|
54
|
+
// producing the very constraints and acknowledgements this turn must read.
|
|
55
|
+
if (this.repo.list('tree_jobs', j => ['learning', 'consolidation'].includes(j.role) && j.nodeIds.includes(nodeId) && !['committed', 'failed', 'invalid_output', 'stale', 'cancelled'].includes(j.status)).length) fail('NODE_BUSY', 'Earlier learning/summary must settle before freezing the next turn');
|
|
56
|
+
const turns = this.experience.turns(nodeId, { includeInterrupted: false });
|
|
57
|
+
for (const prior of turns.slice(0, turns.findIndex(t => t.id === turnId))) {
|
|
58
|
+
const previous = this.existing(prior, nodeId);
|
|
59
|
+
if (!previous || !['committed', 'failed', 'invalid_output', 'stale', 'cancelled'].includes(previous.status)) fail('NODE_ORDER', 'Prior own experience has not been processed');
|
|
60
|
+
}
|
|
61
|
+
const material = this.experience.material(turn); if (material.revokedRefs.length) fail('SOURCE_CHANGED', 'Original experience was withdrawn');
|
|
62
|
+
const context = this.builder.build([nodeId], 'learning'), rows = this.builder.activeItems(context.ancestorPaths.flat(), 'learning');
|
|
63
|
+
const priorTurns = turns.slice(0, turns.findIndex(t => t.id === turnId));
|
|
64
|
+
const observed = this.outputProtocol === 'correction-v1' ? { proposals: [], fragments: [], sources: [] } : this.observations.freeze(nodeId, priorTurns.map(t => t.id));
|
|
65
|
+
const previousSources = this.outputProtocol === 'correction-v1' && priorTurns.length ? this.experience.material(priorTurns.at(-1)).sources : priorTurns.flatMap(t => t.userSourceRefs.map(this.resolveSource)).filter(Boolean);
|
|
66
|
+
const sources = unique([...material.sources, ...previousSources, ...observed.sources]);
|
|
67
|
+
const annotations = jsonClone(this.annotations(turn, nodeId));
|
|
68
|
+
if (annotations.correction) {
|
|
69
|
+
const c = annotations.correction;
|
|
70
|
+
if (!Array.isArray(c.itemIds) || !Array.isArray(c.sourceIds) || !c.sourceIds.length || c.sourceIds.some(id => !material.sources.some(s => s.id === id && s.role === 'user' && s.origin === 'user'))) fail('CORRECTION_REQUIRED', 'Correction must be bound to the current actual user input');
|
|
71
|
+
}
|
|
72
|
+
const scopedItems = this.repo.list('tree_items', i => context.ancestorPaths.flat().includes(i.nodeId));
|
|
73
|
+
const historicalIds = (session?.history ?? []).flatMap(h => ['understandings', 'methods', 'supplements'].flatMap(k => h.receipt?.[k]?.itemIds ?? []));
|
|
74
|
+
const observedIds = new Set([...scopedItems.map(i => i.id), ...historicalIds]);
|
|
75
|
+
const receipts = this.repo.list('tree_change_events', e => e.ordinal > (session?.appliedChangeOrdinal ?? 0) && e.entityIds.some(key => key.startsWith('tree_items:') && observedIds.has(key.slice(11)) || key === 'tree_nodes:' + nodeId || key === 'tree_turns:' + turnId));
|
|
76
|
+
const rejectedOutputs = session ? this.repo.list('tree_jobs', j => j.role === 'learning' && j.nativeSessionId === session.sessionId && ['invalid_output', 'failed'].includes(j.status))
|
|
77
|
+
.map(j => ({ jobId: j.id, turnId: j.trigger.turnId ?? null, state: j.status, adopted: false, error: j.error ?? null, rawHash: j.rawHash ?? null, nativeLogRef: j.nativeLogRef ?? null })) : [];
|
|
78
|
+
const previousReceipt = session?.history?.at(-1)?.receipt ?? null;
|
|
79
|
+
const input = { inputVersion: CONTRACT, role: 'learning', ...(this.outputProtocol === 'correction-v1' ? { outputProtocol: this.outputProtocol } : {}), scope: { nodeIds: [nodeId], ancestorPaths: context.ancestorPaths, treeRevision: context.treeRevision }, sources, knownItems: rows.map(itemInput),
|
|
80
|
+
allowedIds: { nodeIds: context.ancestorPaths.flat(), fileIds: annotations.fileIds ?? [], subjectIds: [...rows.map(r => r.item.id), ...observed.proposals.map(p => p.id), ...observed.fragments.map(f => f.id)] }, changeReceipts: receipts,
|
|
81
|
+
...(this.outputProtocol === 'correction-v1' ? {} : { observationContext: { proposals: observed.proposals, fragments: observed.fragments } }),
|
|
82
|
+
currentTurn: { id: turnId, sourceIds: material.sources.map(s => s.id), assignmentRevision: turn.assignmentRevision }, constraints: context.segments.filter(s => s.nodeId !== null),
|
|
83
|
+
previousReceipt: rejectedOutputs.length ? { ...(previousReceipt ?? {}), rejectedOutputs } : previousReceipt, epochSeed: this.consolidation.learningSeed(nodeId), rootOnly: nodeId === this.repo.rootId() };
|
|
84
|
+
const revisionSet = { ['tree_turns:' + turnId]: turn.revision };
|
|
85
|
+
for (const row of rows) revisionSet['tree_items:' + row.item.id] = row.item.revision;
|
|
86
|
+
// Node changes invalidate scope, but ordinary newer settled turns need not
|
|
87
|
+
// stale this input. TreeService separately marks affected jobs invalidated.
|
|
88
|
+
const trigger = { kind: 'settled_main_turn', producerRole: 'main', status: 'settled', turnId, watermark: digest([turnId, turn.assignmentRevision]),
|
|
89
|
+
...(annotations.correction ? { correction: annotations.correction } : {}), ...(annotations.directRequirement ? { directRequirement: annotations.directRequirement } : {}) };
|
|
90
|
+
return this.jobs.enqueue({ role: 'learning', key, nodeIds: [nodeId], input, revisionSet, config: this.config('learning', { turnId, nodeIds: [nodeId] }), trigger });
|
|
91
|
+
}
|
|
92
|
+
applyResult(result, { job, requestId }) {
|
|
93
|
+
const output = validateOutput('learning', result, job.input), turn = this.repo.get('tree_turns', job.trigger.turnId);
|
|
94
|
+
if (turn?.status !== 'settled' || !turn.nodeIds.includes(job.nodeIds[0]) || turn.assignmentRevision !== job.input.currentTurn.assignmentRevision) fail('JOB_STALE', 'Experience assignment changed');
|
|
95
|
+
// A current, source-bound correction can produce at most one item. An
|
|
96
|
+
// ordinary statement or a model inference alone cannot become learning.
|
|
97
|
+
const selected = { ...output, understandings: [], methods: [], supplements: [], revisions: [] };
|
|
98
|
+
const revision = output.revisions.find(value => value.correctionEvidence?.quotes?.length || job.trigger.correction?.itemIds?.includes(value.targetId));
|
|
99
|
+
if (revision) {
|
|
100
|
+
selected.understandings = [output.understandings[revision.replacedByIndex]];
|
|
101
|
+
selected.revisions = [{ ...revision, replacedByIndex: 0 }];
|
|
102
|
+
} else if (output.correctionClaim) {
|
|
103
|
+
const { kind, quote } = output.correctionClaim;
|
|
104
|
+
quotedSource(job.input, quote, 'user');
|
|
105
|
+
const partition = kind === 'understanding' ? 'understandings' : 'methods';
|
|
106
|
+
const item = output[partition].find(value => value.sourceIds.includes(quote.sourceId));
|
|
107
|
+
if (item) selected[partition] = [item];
|
|
108
|
+
}
|
|
109
|
+
return this.repo.transaction(() => {
|
|
110
|
+
const items = itemServiceFor(this.repo, this.resolveSource, job, learningCorrections(selected, job.input)), input = proposalInput(selected, job.input);
|
|
111
|
+
const receipt = items.commitProposal({ requestId: digest([requestId, 'items']), expected: job.inputRevisionSet, input }).result;
|
|
112
|
+
const categories = this.signals.recordCategories(output.categories, { job, requestId });
|
|
113
|
+
// Per-turn learning is limited to corrections and feedback categories.
|
|
114
|
+
// Do not promote model-extracted proposals, results or decisions into
|
|
115
|
+
// additional observation records; old records remain available to read.
|
|
116
|
+
const methodIds = receipt.methods.itemIds ?? [];
|
|
117
|
+
this.repo.command('recordLearningOrigins', { requestId, expected: {}, input: { jobId: job.id, receipt, categories } }, program, () => {
|
|
118
|
+
for (const partition of ['understandings', 'methods', 'supplements']) for (const id of receipt[partition].itemIds ?? []) {
|
|
119
|
+
const item = this.repo.get('tree_items', id); this.repo.save('tree_items', { ...item, producerJobId: job.id, producerRole: 'learning', sourceTurnId: turn.id }, item.revision);
|
|
120
|
+
}
|
|
121
|
+
for (const intent of this.repo.list('tree_jobs', j => !j.role && j.kind === 'learning' && j.scopeKey === job.nodeIds[0] && j.input.turnId === turn.id && j.input.assignmentRevision === turn.assignmentRevision && j.status === 'queued')) this.repo.save('tree_jobs', { ...intent, status: 'committed', ownedJobId: job.id }, intent.revision);
|
|
122
|
+
const meta = this.repo.meta();
|
|
123
|
+
if (!meta.progressEvidence?.[turn.id]) this.repo.save('tree_meta', { ...meta, progressEvidence: { ...meta.progressEvidence, [turn.id]: { turnId: turn.id, assignmentRevision: turn.assignmentRevision,
|
|
124
|
+
sourceRefs: this.experience.material(turn).sources.filter(s => s.role === 'assistant' && s.origin === 'model' || s.role === 'tool').map(s => s.ref), state: 'unclassified', confirmedOutputs: [], confirmedDecisions: [] } } }, meta.revision);
|
|
125
|
+
return { receipt, categories };
|
|
126
|
+
});
|
|
127
|
+
if (!this.writers && methodIds.some(id => this.repo.get('tree_items', id).lifecycle === 'draft')) fail('WRITER_ROUTER_REQUIRED', 'Script proposals require the owned writer router');
|
|
128
|
+
const writerJobs = this.writers ? methodIds.flatMap(id => this.writers.enqueueDraft(id, job).map(j => j.id)) : [];
|
|
129
|
+
return { ...receipt, categories, writerJobs };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Routes script creation only. Execution remains the separate native main tool. */
|
|
135
|
+
export class WriterRouter {
|
|
136
|
+
constructor({ repository, jobs, modelConfig, artifacts, createWriterOptions, defineTool }) { Object.assign(this, { repo: repository, jobs, config: modelConfig, artifacts, createWriterOptions, defineTool }); this.instances = new Map(); }
|
|
137
|
+
request(input, { job, callId, nodeId, exposureId }) {
|
|
138
|
+
// B7 supplies these identities after validating the live native main call.
|
|
139
|
+
// Neither the model tool arguments nor generated files select an owner.
|
|
140
|
+
if (job.role !== 'main' || !job.nodeIds.includes(nodeId) || !callId) fail('MAIN_REQUIRED', 'An admitted native main call in this node is required');
|
|
141
|
+
this.jobs.assertCurrent(job);
|
|
142
|
+
const requestId = digest(['main-script-request', job.id, callId]), old = this.repo.get('tree_receipts', digest(['receipt', requestId]));
|
|
143
|
+
if (old) { if (old.result.requestHash !== digest(input)) fail('IDEMPOTENCY_CONFLICT', 'Native script request changed'); return old.result; }
|
|
144
|
+
const sourceRefs = resolveSourceIds(input.evidenceIds, job.input);
|
|
145
|
+
if (!sourceRefs.length) fail('WRITER_REQUEST_SOURCE', 'evidenceIds must include a current evidence id');
|
|
146
|
+
if (input.fileRefs.some(id => !job.input.allowedIds.fileIds?.includes(id))) fail('WRITER_REQUEST_SOURCE', 'fileRefs accepts only supplied logical file ids, not paths. If no file ids were supplied use []; describe workspace paths in purpose for the author to inspect.');
|
|
147
|
+
const scriptRequest = { purpose: input.purpose, evidence: input.evidenceIds, files: input.fileRefs };
|
|
148
|
+
const service = new ItemService({ repository: this.repo, resolveSource: this.artifacts.resolveSource, context: () => ({ actor: program }) });
|
|
149
|
+
return this.repo.transaction(() => {
|
|
150
|
+
let methodId = input.methodId;
|
|
151
|
+
if (methodId) {
|
|
152
|
+
const item = this.repo.get('tree_items', methodId);
|
|
153
|
+
if (!item || item.nodeId !== nodeId || item.kind !== 'method' || item.lifecycle !== 'active' || item.activeVersion !== input.expectedVersion || item.userProtected) fail('METHOD_REQUEST_SCOPE', 'Existing script request must target an own current unprotected method');
|
|
154
|
+
const version = this.repo.get('tree_item_versions', digest(['item_version', item.id, item.activeVersion]));
|
|
155
|
+
service.review({ requestId: digest([requestId, 'draft']), expected: { ['tree_items:' + item.id]: item.revision }, input: { itemId: item.id, version: item.activeVersion, exposureId, operation: 'rewrite', reason: '当前主轮请求制作脚本', sourceRefs,
|
|
156
|
+
windowId: requestId, opportunityRefs: [], replacement: { ...version.content, scriptRequests: [scriptRequest] } } });
|
|
157
|
+
} else {
|
|
158
|
+
const proposed = { kind: 'technique', short: null, long: { name: '当前明确请求的脚本', description: '按当前委托制作并提供调用说明', whenToUse: '', steps: input.purpose, pitfalls: '', verify: '' }, hooks: [], scriptRequests: [scriptRequest],
|
|
159
|
+
expectedEffect: { category: 'omitted', change: 'less', text: '当前明确要求的脚本可供调用' }, sourceRefs };
|
|
160
|
+
const receipt = service.commitProposal({ requestId: digest([requestId, 'draft']), expected: {}, input: { nodeId, understandings: [], methods: [proposed], supplements: [] } }).result;
|
|
161
|
+
if (receipt.methods.status !== 'applied') fail('SCRIPT_REQUEST_INVALID', receipt.methods.reason); methodId = receipt.methods.itemIds[0];
|
|
162
|
+
}
|
|
163
|
+
const jobs = this.enqueueDraft(methodId, job, { explicit: true });
|
|
164
|
+
return this.repo.command('requestMethodScript', { requestId, expected: {}, input }, program, () => ({ methodId, jobId: jobs[0].id, state: 'queued', requestHash: digest(input) })).result;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
enqueueDraft(methodId, parentJob, { explicit = false } = {}) {
|
|
168
|
+
const item = this.repo.get('tree_items', methodId); if (!item || item.lifecycle !== 'draft') return [];
|
|
169
|
+
const bundle = this.repo.get('tree_methods', digest(['method', methodId, item.draftVersion]));
|
|
170
|
+
return bundle.pendingScriptRequests.map((request, requestIndex) => {
|
|
171
|
+
const key = digest(['write-method-script', methodId, item.draftVersion, requestIndex]), prior = this.repo.get('tree_jobs', digest(['model_job', 'script_writer', key])); if (prior) return prior;
|
|
172
|
+
const sources = parentJob.input.sources.filter(s => request.evidence.includes(s.id));
|
|
173
|
+
if (sources.length !== new Set(request.evidence).size || !sources.length || request.files.some(id => !parentJob.input.allowedIds.fileIds?.includes(id))) fail('WRITER_REQUEST_SOURCE', 'Script request must bind its source and allowed file references');
|
|
174
|
+
const binding = { methodId, draftVersion: item.draftVersion, requestIndex, scriptId: digest(['method-script', methodId, item.draftVersion, requestIndex]), version: 1 };
|
|
175
|
+
return this.jobs.enqueue({ role: 'script_writer', key, nodeIds: [item.nodeId], revisionSet: { ['tree_items:' + item.id]: item.revision }, config: this.config('script_writer', { parentJobId: parentJob.id, nodeIds: [item.nodeId] }),
|
|
176
|
+
trigger: { kind: explicit ? 'explicit_script_request' : 'learned_script_request', producerRole: parentJob.role, methodId, parentJobId: parentJob.id },
|
|
177
|
+
input: { scope: { nodeIds: [item.nodeId], ancestorPaths: [this.repo.ancestors(item.nodeId).map(n => n.id)], treeRevision: this.repo.meta().treeRevision }, sources, knownItems: [], allowedIds: { nodeIds: [item.nodeId], fileIds: request.files, subjectIds: [] }, changeReceipts: [], request, binding } });
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
writer(job) {
|
|
181
|
+
if (job.role !== 'script_writer') fail('WRITER_BINDING', 'Only an owned writer job may mount these tools');
|
|
182
|
+
let writer = this.instances.get(job.id);
|
|
183
|
+
if (!writer) {
|
|
184
|
+
const options = this.createWriterOptions(job);
|
|
185
|
+
writer = new ScriptWriter({ ...options, repository: this.repo, artifacts: this.artifacts, binding: { ...job.input.binding, jobId: job.id }, assertJob: id => {
|
|
186
|
+
const current = this.jobs.get(id); this.jobs.assertCurrent(current); return ['prepared', 'submitted', 'result_received', 'validated', 'committed'].includes(current.status);
|
|
187
|
+
} }); this.instances.set(job.id, writer);
|
|
188
|
+
}
|
|
189
|
+
return writer;
|
|
190
|
+
}
|
|
191
|
+
mount(agentContext, job) { this.writer(job).mount(agentContext, this.defineTool); }
|
|
192
|
+
applyResult(output, { job }) { return this.writer(job).verifyFinal(output); }
|
|
193
|
+
activateReady(methodId, draftVersion) {
|
|
194
|
+
const item = this.repo.get('tree_items', methodId);
|
|
195
|
+
if (item?.lifecycle === 'active' && item.activeVersion === draftVersion + 1) return { itemId: item.id, version: item.activeVersion, duplicate: true };
|
|
196
|
+
if (!item || item.lifecycle !== 'draft' || item.draftVersion !== draftVersion) fail('JOB_STALE', 'Method draft changed before activation');
|
|
197
|
+
const bundle = this.repo.get('tree_methods', digest(['method', methodId, draftVersion]));
|
|
198
|
+
const jobs = bundle.pendingScriptRequests.map((_, index) => this.repo.get('tree_jobs', digest(['model_job', 'script_writer', digest(['write-method-script', methodId, draftVersion, index])])));
|
|
199
|
+
if (jobs.some(j => !j || j.status !== 'committed')) return { itemId: methodId, state: jobs.some(j => j?.status === 'unknown') ? 'unknown' : jobs.some(j => ['failed', 'invalid_output', 'stale', 'cancelled'].includes(j?.status)) ? 'failed' : 'queued' };
|
|
200
|
+
return this.artifacts.activate({ methodId, draftVersion, artifactIds: jobs.map(j => j.resultRef.artifactId), requestId: digest(['activate-method-scripts', methodId, draftVersion]) }).result;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { canonicalJson, digest, fail, CATEGORIES, integer, text } from './contracts.mjs';
|
|
2
|
+
import { effectiveSignals, signalPolarity } from './signals.mjs';
|
|
3
|
+
|
|
4
|
+
export const METRIC_VERSION = 'arcfold.observed-metrics/1';
|
|
5
|
+
const program = { kind: 'program', id: 'tree-metrics' };
|
|
6
|
+
const unique = values => [...new Map(values.map(v => [canonicalJson(v), v])).values()];
|
|
7
|
+
export function ratio(numerator, denominator) {
|
|
8
|
+
if (!denominator) return { numerator, denominator, value: null, interval: null };
|
|
9
|
+
const p = numerator / denominator, z = 1.95996398454, d = 1 + z * z / denominator, center = (p + z * z / (2 * denominator)) / d, margin = z * Math.sqrt(p * (1 - p) / denominator + z * z / (4 * denominator * denominator)) / d;
|
|
10
|
+
return { numerator, denominator, value: p, interval: [Math.max(0, center - margin), Math.min(1, center + margin)], intervalMethod: 'Wilson95-descriptive-not-causal' };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class MetricsService {
|
|
14
|
+
constructor({ repository, experience, signals }) { Object.assign(this, { repo: repository, experience, signals }); this.resolveSource = signals.resolveSource; }
|
|
15
|
+
ordinal(kind, id, { first = false } = {}) {
|
|
16
|
+
const values = this.repo.list('tree_change_events', e => e.entityIds.includes(kind + ':' + id) && (!first || e.beforeVersions[e.entityIds.indexOf(kind + ':' + id)] === 0)).map(e => e.ordinal);
|
|
17
|
+
return values.length ? (first ? Math.min(...values) : Math.max(...values)) : 0;
|
|
18
|
+
}
|
|
19
|
+
turns(scopeNodeId = null) {
|
|
20
|
+
return this.repo.list('tree_turns', t => t.status === 'settled' && (!scopeNodeId || t.nodeIds.includes(scopeNodeId))).map(t => ({ ...t, ordinal: this.ordinal('tree_turns', t.id, { first: true }) })).sort((a, b) => a.ordinal - b.ordinal || a.id.localeCompare(b.id));
|
|
21
|
+
}
|
|
22
|
+
proposal({ proposalRef, nodeIds, requestId, start = 0, end }) {
|
|
23
|
+
const source = this.resolveSource(proposalRef); if (source?.role !== 'assistant' || source.origin !== 'model') fail('PROPOSAL_SOURCE', 'An actual assistant proposal source is required');
|
|
24
|
+
end ??= source.text.length; integer(start, 'start', 0, source.text.length); integer(end, 'end', start + 1, source.text.length);
|
|
25
|
+
const turn = this.repo.list('tree_turns', t => t.status === 'settled' && t.experienceRefs?.some(ref => digest(ref) === digest(proposalRef)))[0];
|
|
26
|
+
if (!turn || !nodeIds.length || nodeIds.some(id => !turn.nodeIds.includes(id))) fail('PROPOSAL_SCOPE', 'A proposal must belong to the actual settled turn');
|
|
27
|
+
const id = digest(['initiative-proposal', proposalRef, start, end]);
|
|
28
|
+
return this.repo.command('recordInitiativeProposal', { requestId, expected: {}, input: { proposalRef, nodeIds, start, end } }, program, () => {
|
|
29
|
+
const meta = this.repo.meta(), old = meta.proposals?.[id];
|
|
30
|
+
if (!old || nodeIds.some(n => !old.nodeIds.includes(n))) this.repo.save('tree_meta', { ...meta, proposals: { ...meta.proposals, [id]: old ? { ...old, nodeIds: [...new Set([...old.nodeIds, ...nodeIds])] } : { id, proposalRef, start, end, nodeIds, turnId: turn.id, state: 'unknown', responseRefs: [], ordinal: meta.lastEventOrdinal + 1 } } }, meta.revision); return { proposalId: id };
|
|
31
|
+
}).result;
|
|
32
|
+
}
|
|
33
|
+
respond({ proposalId, outcome, responseRefs, requestId }) {
|
|
34
|
+
if (!['adopted', 'rejected', 'ignored', 'unknown'].includes(outcome)) fail('PROPOSAL_OUTCOME', 'Unknown proposal outcome');
|
|
35
|
+
const proposal = this.repo.meta().proposals?.[proposalId]; if (!proposal) fail('PROPOSAL_NOT_FOUND', 'Proposal missing');
|
|
36
|
+
if (outcome !== 'unknown' && !responseRefs.length || responseRefs.some(ref => { const s = this.resolveSource(ref); return s?.role !== 'user' || s.origin !== 'user'; })) fail('USER_SOURCE_REQUIRED', 'A known proposal outcome needs actual human response');
|
|
37
|
+
for (const ref of responseRefs) this.responseTurn(proposal.turnId, proposal.nodeIds, ref);
|
|
38
|
+
// Called only from trusted source-bound classification, not text matching.
|
|
39
|
+
return this.repo.command('recordProposalOutcome', { requestId, expected: {}, input: { proposalId, outcome, sourceRefs: responseRefs } }, program, () => {
|
|
40
|
+
const meta = this.repo.meta(), old = meta.proposals[proposalId];
|
|
41
|
+
this.repo.save('tree_meta', { ...meta, proposals: { ...meta.proposals, [proposalId]: { ...old, state: outcome, responseRefs, history: [...(old.history ?? []), { outcome, responseRefs, ordinal: meta.lastEventOrdinal + 1 }] } } }, meta.revision); return { proposalId, outcome };
|
|
42
|
+
}).result;
|
|
43
|
+
}
|
|
44
|
+
responseTurn(beforeTurnId, nodeIds, ref) {
|
|
45
|
+
const before = this.repo.get('tree_turns', beforeTurnId), response = this.repo.list('tree_turns', t => t.status === 'settled' && t.userSourceRefs.some(r => digest(r) === digest(ref)))[0];
|
|
46
|
+
if (!before || !response || !response.nodeIds.some(id => nodeIds.includes(id) && before.nodeIds.includes(id))) fail('PROPOSAL_SCOPE', 'Response belongs to another goal');
|
|
47
|
+
if (this.ordinal('tree_turns', response.id, { first: true }) <= this.ordinal('tree_turns', before.id, { first: true })) fail('RESPONSE_ORDER', 'A response must follow the actual result');
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
// The integration classifier must identify a task fragment explicitly. Broad
|
|
51
|
+
// proceeded/redo labels alone cannot establish that this was a first attempt.
|
|
52
|
+
fragment({ firstResultRef, nodeIds, fragmentKey, requestId, outputPaths = [], span }) {
|
|
53
|
+
text(fragmentKey, 'fragmentKey', 256); const source = this.resolveSource(firstResultRef);
|
|
54
|
+
const turn = this.repo.list('tree_turns', t => t.status === 'settled' && t.experienceRefs?.some(r => digest(r) === digest(firstResultRef)))[0];
|
|
55
|
+
if (source?.role !== 'assistant' || source.origin !== 'model' || !turn || !nodeIds.length || nodeIds.some(id => !turn.nodeIds.includes(id))) fail('FRAGMENT_SOURCE', 'A fragment needs its actual first assistant result');
|
|
56
|
+
if (span) { integer(span.start, 'start', 0, source.text.length); integer(span.end, 'end', span.start + 1, source.text.length); }
|
|
57
|
+
if (!Array.isArray(outputPaths) || outputPaths.some(p => typeof p !== 'string' || !p || p.startsWith('/') || p.split('/').includes('..'))) fail('FRAGMENT_OUTPUTS', 'Output paths must be workspace-relative evidence from the trusted result classifier');
|
|
58
|
+
const id = digest(['task-fragment', firstResultRef, fragmentKey]);
|
|
59
|
+
return this.repo.command('recordTaskFragment', { requestId, expected: {}, input: { firstResultRef, nodeIds, fragmentKey, outputPaths, ...(span ? { span } : {}) } }, program, () => {
|
|
60
|
+
const meta = this.repo.meta(), old = meta.taskFragments?.[id];
|
|
61
|
+
if (!old || nodeIds.some(n => !old.nodeIds.includes(n))) this.repo.save('tree_meta', { ...meta, taskFragments: { ...meta.taskFragments, [id]: old ? { ...old, nodeIds: [...new Set([...old.nodeIds, ...nodeIds])] } : { id, firstResultRef, nodeIds, turnId: turn.id, fragmentKey, outputPaths, feedbackRefs: [], ...(span ? { span } : {}) } } }, meta.revision); return { fragmentId: id };
|
|
62
|
+
}).result;
|
|
63
|
+
}
|
|
64
|
+
fragmentFeedback({ fragmentId, responseRefs, requestId, outcome }) {
|
|
65
|
+
const fragment = this.repo.meta().taskFragments?.[fragmentId]; if (!fragment) fail('FRAGMENT_MISSING', 'Task fragment missing');
|
|
66
|
+
if (!responseRefs.length) fail('USER_SOURCE_REQUIRED', 'Actual feedback is required');
|
|
67
|
+
if (outcome !== undefined && !['proceeded', 'redo', 'unknown'].includes(outcome)) fail('FRAGMENT_OUTCOME', 'Unknown result feedback');
|
|
68
|
+
for (const ref of responseRefs) { const s = this.resolveSource(ref); if (s?.role !== 'user' || s.origin !== 'user') fail('USER_SOURCE_REQUIRED', 'Actual human feedback required'); this.responseTurn(fragment.turnId, fragment.nodeIds, ref); }
|
|
69
|
+
return this.repo.command('bindFragmentFeedback', { requestId, expected: {}, input: { fragmentId, responseRefs, ...(outcome === undefined ? {} : { outcome }) } }, program, () => {
|
|
70
|
+
const meta = this.repo.meta(), old = meta.taskFragments[fragmentId]; this.repo.save('tree_meta', { ...meta, taskFragments: { ...meta.taskFragments, [fragmentId]: { ...old, feedbackRefs: unique([...old.feedbackRefs, ...responseRefs]),
|
|
71
|
+
...(outcome === undefined ? {} : { classifiedFeedback: [...(old.classifiedFeedback ?? []), { outcome, responseRefs, ordinal: meta.lastEventOrdinal + 1 }] }) } } }, meta.revision); return { fragmentId };
|
|
72
|
+
}).result;
|
|
73
|
+
}
|
|
74
|
+
freeze(scopeNodeId = null, { afterOrdinal = 0, throughOrdinal = this.repo.meta().lastEventOrdinal } = {}) {
|
|
75
|
+
integer(afterOrdinal, 'afterOrdinal', 0); integer(throughOrdinal, 'throughOrdinal', afterOrdinal);
|
|
76
|
+
if (scopeNodeId && !this.repo.get('tree_nodes', scopeNodeId)) fail('NODE_NOT_FOUND', 'Metric scope missing');
|
|
77
|
+
let turns = this.turns(scopeNodeId).filter(t => t.ordinal > afterOrdinal && t.ordinal <= throughOrdinal); if (!scopeNodeId && turns.length > 100) { turns = turns.slice(-100); afterOrdinal = Math.max(afterOrdinal, turns[0].ordinal - 1); }
|
|
78
|
+
const turnIds = new Set(turns.map(t => t.id)), refs = unique(turns.flatMap(t => t.userSourceRefs)), validRefs = refs.filter(ref => { const s = this.resolveSource(ref); return s?.role === 'user' && s.origin === 'user'; });
|
|
79
|
+
const messages = new Set(validRefs.map(digest)), signals = effectiveSignals(this.repo, this.resolveSource).filter(s => s.sourceRefs.some(ref => messages.has(digest(ref))));
|
|
80
|
+
const categoryRefs = names => new Set(signals.filter(s => names.includes(s.category)).flatMap(s => s.sourceRefs.filter(r => messages.has(digest(r))).map(digest)));
|
|
81
|
+
const counts = Object.fromEntries(CATEGORIES.map(name => [name, categoryRefs([name]).size]));
|
|
82
|
+
const negative = categoryRefs(CATEGORIES.filter(c => signalPolarity(c) === 'negative'));
|
|
83
|
+
const fragments = Object.values(this.repo.meta().taskFragments ?? {}).filter(f => (!scopeNodeId || f.nodeIds.includes(scopeNodeId)) && this.resolveSource(f.firstResultRef));
|
|
84
|
+
const restores = Object.values(this.repo.meta().restoreIntents ?? {}).filter(r => r.state === 'completed' && r.changes?.length).flatMap(r => {
|
|
85
|
+
const turnId = this.repo.get('tree_snapshots', r.snapshotId)?.logicalTurnId;
|
|
86
|
+
const ordinal = this.repo.list('tree_change_events', event => { const receipt = event.commandType === 'recordTreeRestore' && this.repo.get('tree_receipts', event.receiptId); return receipt?.result?.id === r.id && receipt.result.state === 'completed'; })[0]?.ordinal;
|
|
87
|
+
return turnId && ordinal ? [{ id: r.id, turnId, ordinal, paths: r.changes.map(c => c.path) }] : [];
|
|
88
|
+
});
|
|
89
|
+
const assessedFragments = fragments.flatMap(f => {
|
|
90
|
+
const feedback = this.turns().flatMap(t => t.userSourceRefs.filter(r => f.feedbackRefs.some(ref => digest(ref) === digest(r))).map(ref => ({ ref, ordinal: t.ordinal }))).sort((a, b) => a.ordinal - b.ordinal);
|
|
91
|
+
// A human message can accept result A and reject result B. Source-bound
|
|
92
|
+
// per-fragment classifications take precedence over broad message labels.
|
|
93
|
+
const classified = feedback.map(entry => ({ ...entry, classification: f.classifiedFeedback?.find(c => c.ordinal <= throughOrdinal && c.responseRefs.some(r => digest(r) === digest(entry.ref))) }));
|
|
94
|
+
const first = classified.find(entry => entry.classification ? entry.classification.outcome !== 'unknown' : effectiveSignals(this.repo, this.resolveSource).some(s => ['proceeded', 'redo'].includes(s.category) && s.sourceRefs.some(r => digest(r) === digest(entry.ref))));
|
|
95
|
+
const restore = restores.filter(r => r.turnId === f.turnId && r.paths.some(path => f.outputPaths?.includes(path))).sort((a, b) => a.ordinal - b.ordinal)[0];
|
|
96
|
+
if (restore && (!first || restore.ordinal < first.ordinal)) return restore.ordinal > afterOrdinal && restore.ordinal <= throughOrdinal ? [{ id: f.id, outcome: 'redo', restoreId: restore.id }] : [];
|
|
97
|
+
if (!first || !messages.has(digest(first.ref))) return [];
|
|
98
|
+
const categories = signals.filter(s => s.sourceRefs.some(r => digest(r) === digest(first.ref))).map(s => s.category);
|
|
99
|
+
return [{ id: f.id, outcome: first.classification?.outcome ?? (categories.includes('redo') ? 'redo' : 'proceeded'), ref: first.ref }];
|
|
100
|
+
});
|
|
101
|
+
const boundFeedback = new Set(fragments.flatMap(f => f.feedbackRefs).map(digest));
|
|
102
|
+
const proposals = Object.values(this.repo.meta().proposals ?? {}).filter(p => turnIds.has(p.turnId) && this.resolveSource(p.proposalRef)).map(p => {
|
|
103
|
+
const response = p.history?.filter(h => h.ordinal <= throughOrdinal).at(-1); return { ...p, state: response?.outcome ?? 'unknown', responseRefs: response?.responseRefs ?? [] };
|
|
104
|
+
});
|
|
105
|
+
const decidedProposals = proposals.filter(p => ['adopted', 'rejected', 'ignored'].includes(p.state) && p.responseRefs.length && p.responseRefs.every(this.resolveSource));
|
|
106
|
+
const inferred = new Map(), methods = new Map();
|
|
107
|
+
for (const item of this.repo.list('tree_items', i => !scopeNodeId || i.nodeId === scopeNodeId)) {
|
|
108
|
+
const versions = this.repo.list('tree_item_versions', v => v.itemId === item.id);
|
|
109
|
+
if (item.kind === 'understanding') for (const version of versions.filter(v => v.sourceKind === 'assistant_inferred')) {
|
|
110
|
+
const confirmation = versions.find(v => v.supersedes === version.version && v.confirmationRef && this.ordinal('tree_item_versions', v.id, { first: true }) > afterOrdinal && this.ordinal('tree_item_versions', v.id, { first: true }) <= throughOrdinal);
|
|
111
|
+
const evaluations = (item.reviews ?? []).filter(r => r.version === version.version && ['met', 'not_met'].includes(r.result) && r.opportunityRefs.some(ref => messages.has(digest(ref))));
|
|
112
|
+
if (confirmation || evaluations.length) inferred.set(item.id + ':' + version.version, confirmation ? 'met' : evaluations.at(-1).result);
|
|
113
|
+
}
|
|
114
|
+
if (item.kind === 'method') for (const evaluation of item.evaluations ?? []) {
|
|
115
|
+
const job = this.repo.list('tree_jobs', j => j.role === 'verification' && j.status === 'committed' && j.input.window?.id === evaluation.windowId)[0];
|
|
116
|
+
if (job && this.ordinal('tree_jobs', job.id) > afterOrdinal && this.ordinal('tree_jobs', job.id) <= throughOrdinal && job.input.window.throughTurnIds.some(id => turnIds.has(id))) methods.set(item.id + ':' + evaluation.version, evaluation);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const methodEvaluated = [...methods.values()].filter(e => e.opportunityCount > 0 && e.result !== 'unclear');
|
|
120
|
+
const hooks = this.signals.hookCandidates().filter(h => turnIds.has(h.logicalTurnId));
|
|
121
|
+
const unrecovered = hooks.filter(h => !h.recovered && !signals.some(s => s.category === 'omitted' && this.repo.list('tree_turns', t => t.sessionId === this.repo.get('tree_turns', h.logicalTurnId)?.sessionId && t.nativeTurn > this.repo.get('tree_turns', h.logicalTurnId)?.nativeTurn).sort((a, b) => a.nativeTurn - b.nativeTurn)[0]?.userSourceRefs.some(ref => s.sourceRefs.some(r => digest(r) === digest(ref)))));
|
|
122
|
+
const recurrentRefs = new Set(this.repo.list('tree_clusters', c => c.recurrence && !c.needsRecheck).flatMap(c => c.evidenceRefs ?? []).map(digest).filter(ref => negative.has(ref)));
|
|
123
|
+
const learned = this.repo.list('tree_items', i => ['understanding', 'method'].includes(i.kind) && (!scopeNodeId || i.nodeId === scopeNodeId) && this.ordinal('tree_items', i.id, { first: true }) <= throughOrdinal);
|
|
124
|
+
// Lifecycle rows are mutable; do not invent historical state when asked to
|
|
125
|
+
// reconstruct an older watermark. Saved live windows retain their snapshot.
|
|
126
|
+
const currentLifecycle = throughOrdinal === this.repo.meta().lastEventOrdinal;
|
|
127
|
+
const learningInventory = { learned: learned.length, methodsCreated: learned.filter(i => i.kind === 'method' && this.ordinal('tree_items', i.id, { first: true }) > afterOrdinal).length, invalidOrWithdrawn: currentLifecycle ? learned.filter(i => ['invalid', 'withdrawn'].includes(i.lifecycle) || i.validation === 'contradicted').length : null, historicalStateMissing: !currentLifecycle };
|
|
128
|
+
const rates = { understandingProblems: ratio(categoryRefs(['not_understood', 'misread', 'restart']).size, messages.size), negativeFeedback: ratio(negative.size, messages.size),
|
|
129
|
+
confirmedInferences: ratio([...inferred.values()].filter(v => v === 'met').length, inferred.size), adoption: ratio(decidedProposals.filter(p => p.state === 'adopted').length, decidedProposals.length),
|
|
130
|
+
firstPass: ratio(assessedFragments.filter(f => f.outcome === 'proceeded').length, assessedFragments.length), methodExpectations: ratio(methodEvaluated.filter(e => e.result === 'met').length, methodEvaluated.length), recurrence: ratio(recurrentRefs.size, negative.size) };
|
|
131
|
+
const scope = scopeNodeId ?? 'global', identity = [scope, afterOrdinal, throughOrdinal, turns.map(t => [t.id, t.assignmentRevision]), signals.map(s => s.id), [...inferred], [...methods], proposals, assessedFragments];
|
|
132
|
+
return { id: digest(['metric-window', identity]), scope, fromOrdinal: afterOrdinal, toOrdinal: throughOrdinal, observations: { logicalTurns: turns.length, userMessages: messages.size, turnIds: [...turnIds], counts,
|
|
133
|
+
strong: categoryRefs(['approved', 'adopted_proposal', 'confirmed_inference']).size, weakProgress: counts.proceeded, unresolvedProposals: proposals.filter(p => p.state === 'unknown').length, hookRecovered: hooks.filter(h => h.recovered).length, hookOmissionCandidates: unrecovered.length,
|
|
134
|
+
methodUnclear: [...methods.values()].filter(e => e.result === 'unclear').length, learningInventory, restores: restores.filter(r => r.ordinal > afterOrdinal && r.ordinal <= throughOrdinal && turnIds.has(r.turnId)).length, unboundRestores: restores.filter(r => r.ordinal > afterOrdinal && r.ordinal <= throughOrdinal && turnIds.has(r.turnId) && !fragments.some(f => f.turnId === r.turnId && r.paths.some(path => f.outputPaths?.includes(path)))).length, rates, growth: this.growth({ afterOrdinal, throughOrdinal, scopeNodeId }) },
|
|
135
|
+
numerators: Object.fromEntries(Object.entries(rates).map(([k, v]) => [k, v.numerator])), denominators: Object.fromEntries(Object.entries(rates).map(([k, v]) => [k, v.denominator])),
|
|
136
|
+
missing: { revokedUserSources: refs.length - validRefs.length, unboundFirstAttemptFeedback: [...categoryRefs(['proceeded', 'redo'])].filter(ref => !boundFeedback.has(ref)).length, unassessedInferences: this.repo.list('tree_items', i => i.kind === 'understanding' && i.validation === 'unverified' && (!scopeNodeId || i.nodeId === scopeNodeId)).length, unboundAdoptionSignals: signals.filter(s => s.category === 'adopted_proposal' && !s.subjectRefs.some(id => proposals.some(p => p.id === id))).length },
|
|
137
|
+
algorithmVersion: METRIC_VERSION, needsRecheck: false, caveat: '观察统计;沉默不等于认可,样本间未保证独立,不构成因果收益证明。' };
|
|
138
|
+
}
|
|
139
|
+
saveWindow(scopeNodeId, options) {
|
|
140
|
+
const value = this.freeze(scopeNodeId, options), existing = this.repo.get('tree_metric_windows', value.id); if (existing && !existing.needsRecheck) return existing;
|
|
141
|
+
return this.repo.command('recordMetricWindow', { requestId: digest(['metrics', value.id, existing?.revision ?? 0]), expected: {}, input: { scopeNodeId, from: value.fromOrdinal, to: value.toOrdinal } }, program, () => {
|
|
142
|
+
this.repo.save('tree_metric_windows', value, existing?.revision ?? null); return { windowId: value.id };
|
|
143
|
+
}).result.windowId && this.repo.get('tree_metric_windows', value.id);
|
|
144
|
+
}
|
|
145
|
+
compare(previous, current) {
|
|
146
|
+
if (!previous || !current || previous.needsRecheck || current.needsRecheck || previous.algorithmVersion !== current.algorithmVersion || previous.scope !== current.scope || previous.toOrdinal !== current.fromOrdinal || previous.observations.logicalTurns < 10 || current.observations.logicalTurns < 10) return { comparable: false, reason: '需要两个相邻、算法相同且各至少10轮的有效窗口', trends: {} };
|
|
147
|
+
const trends = Object.fromEntries(Object.entries(current.observations.rates).map(([name, now]) => { const before = previous.observations.rates[name]; return [name, now.value === null || before.value === null ? null : { previous: before.value, current: now.value, delta: now.value - before.value }]; }));
|
|
148
|
+
return { comparable: true, trends, notImproved: Object.entries(trends).filter(([name, value]) => value && (['understandingProblems', 'negativeFeedback', 'recurrence'].includes(name) ? value.delta >= 0 : value.delta <= 0)).map(([name]) => name), caveat: current.caveat };
|
|
149
|
+
}
|
|
150
|
+
observeTree(receipt, beforeNodes, { logicalTurnId = receipt.result?.turnId ?? null } = {}) {
|
|
151
|
+
const event = this.repo.get('tree_change_events', receipt.eventIds[0]); if (!event) fail('TREE_RECEIPT', 'Actual tree command receipt required');
|
|
152
|
+
if (logicalTurnId && !this.repo.get('tree_turns', logicalTurnId)) fail('TURN_NOT_FOUND', 'Tree observation must belong to a real turn');
|
|
153
|
+
const key = digest(['tree-observation', receipt.id]); if (this.repo.meta().treeObservations?.[key]) return this.repo.meta().treeObservations[key];
|
|
154
|
+
const observations = [];
|
|
155
|
+
for (const id of event.entityIds.filter(k => k.startsWith('tree_nodes:')).map(k => k.slice(11))) {
|
|
156
|
+
if (id === this.repo.rootId()) continue; const before = beforeNodes.find(n => n.id === id), after = this.repo.get('tree_nodes', id);
|
|
157
|
+
for (const type of ['created', 'name', 'parentId', 'status', 'deleted']) {
|
|
158
|
+
if (type === 'created' ? Boolean(before) : !before || canonicalJson(before[type] ?? null) === canonicalJson(after[type] ?? null)) continue;
|
|
159
|
+
const origin = type === 'status' && after.statusOrigin === 'cascade' ? 'cascade' : event.actor.kind === 'user' ? 'user' : event.actor.role === 'attribution' || event.commandType === 'applyAttribution' ? 'attribution' : 'assistant';
|
|
160
|
+
observations.push({ id: digest([key, id, type]), nodeId: id, type, before: before?.[type] ?? null, after: type === 'created' ? after.parentId : after[type], origin, ordinal: event.ordinal, sourceRefs: event.sourceRefs, commandReceiptId: receipt.id, logicalTurnId, reversalOf: receipt.result?.reversalOf ?? null,
|
|
161
|
+
beforeStatusOrigin: before?.statusOrigin ?? null, beforeCascadeFromId: before?.cascadeFromId ?? null, afterRevision: after.revision });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.repo.command('observeTreeChanges', { requestId: key, expected: {}, input: { receiptId: receipt.id } }, program, () => { const meta = this.repo.meta(); this.repo.save('tree_meta', { ...meta, treeObservations: { ...meta.treeObservations, [key]: observations } }, meta.revision); return { observations }; }); return observations;
|
|
165
|
+
}
|
|
166
|
+
growth({ afterOrdinal = 0, throughOrdinal = this.repo.meta().lastEventOrdinal, scopeNodeId = null } = {}) {
|
|
167
|
+
const observations = Object.values(this.repo.meta().treeObservations ?? {}).flat().filter(o => o.ordinal > afterOrdinal && o.ordinal <= throughOrdinal && (!scopeNodeId || o.nodeId === scopeNodeId));
|
|
168
|
+
const all = Object.values(this.repo.meta().treeObservations ?? {}).flat();
|
|
169
|
+
const unreversed = observations.filter(o => !o.reversalOf && o.origin !== 'cascade' && !this.repo.get('tree_nodes', o.nodeId)?.deleted && !all.some(later => later.ordinal > o.ordinal && later.nodeId === o.nodeId && later.type === o.type && canonicalJson(later.after) === canonicalJson(o.before)));
|
|
170
|
+
const eligible = unreversed.filter(o => o.origin === 'user'), weak = unreversed.filter(o => o.origin === 'attribution' && o.type === 'created' && this.turns().some(t => t.ordinal > o.ordinal));
|
|
171
|
+
const turns = this.turns(scopeNodeId).filter(t => t.ordinal > afterOrdinal && t.ordinal <= throughOrdinal), assignedNodeCount = new Set(turns.flatMap(t => t.nodeIds).filter(id => id !== this.repo.rootId())).size;
|
|
172
|
+
const completions = unreversed.filter(o => o.type === 'status' && o.after === 'completed').map(o => { const created = all.find(c => c.nodeId === o.nodeId && c.type === 'created' && c.ordinal <= o.ordinal); return { nodeId: o.nodeId, origin: o.origin, userTurnsSinceCreation: created ? this.turns(o.nodeId).filter(t => t.ordinal > created.ordinal && t.ordinal <= o.ordinal).length : null }; });
|
|
173
|
+
return { raw: observations.length, excludingCascadeAndReversed: unreversed.length, confirmedUserChanges: eligible.length, weakUnretractedCreations: weak.length, weakAssistantCompletions: completions.filter(c => c.origin !== 'user').length, completions,
|
|
174
|
+
assignedNodeCount, logicalTurns: turns.length, assignedNodesPer100Turns: turns.length >= 100 ? assignedNodeCount * 100 / turns.length : null,
|
|
175
|
+
userStatusChoices: Object.fromEntries(['active', 'paused', 'completed', 'abandoned'].map(status => [status, eligible.filter(o => o.type === 'status' && o.after === status).length])), byType: Object.fromEntries(['created', 'name', 'parentId', 'status'].map(type => [type, eligible.filter(o => o.type === type).length])), observations, valueJudgment: null };
|
|
176
|
+
}
|
|
177
|
+
stagnation(nodeId) {
|
|
178
|
+
const node = this.repo.get('tree_nodes', nodeId), turns = this.turns(nodeId).slice(-10); if (node?.status !== 'active' || turns.length < 10) return null;
|
|
179
|
+
const progress = turns.map(t => this.repo.meta().progressEvidence?.[t.id] ?? { turnId: t.id, state: 'unclassified', sourceRefs: this.experience.material(t).sources.map(s => s.ref), confirmedOutputs: [], confirmedDecisions: [] });
|
|
180
|
+
if (progress.some(p => p.confirmedOutputs.length || p.confirmedDecisions.length)) return null;
|
|
181
|
+
return { nodeId, turnIds: turns.map(t => t.id), progress, status: 'candidate_only', needsSemanticReview: true };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class ReflectionTriggers {
|
|
186
|
+
constructor({ repository, metrics, clusters, verification }) { Object.assign(this, { repo: repository, metrics, clusters, verification }); }
|
|
187
|
+
once(key, input, enqueue) {
|
|
188
|
+
const old = this.repo.meta().reflectionTriggers?.[key]; if (old) return this.repo.get('tree_jobs', old.jobId);
|
|
189
|
+
return this.repo.transaction(() => {
|
|
190
|
+
const job = enqueue(); this.repo.command('recordReflectionTrigger', { requestId: key, expected: {}, input }, program, () => { const meta = this.repo.meta(); this.repo.save('tree_meta', { ...meta, reflectionTriggers: { ...meta.reflectionTriggers, [key]: { ...input, jobId: job.id } } }, meta.revision); return { jobId: job.id }; }); return job;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
scan(nodeId, { summaryId = null, previousWindow = null, currentWindow = null, parentJobId = null } = {}) {
|
|
194
|
+
const jobs = [];
|
|
195
|
+
for (const category of CATEGORIES.filter(c => c !== 'proceeded')) { const cluster = this.clusters.candidate(category, signalPolarity(category)); if (cluster) jobs.push(this.once(digest(['reflect-cluster', cluster.id, cluster.revision]), { kind: 'cluster', clusterId: cluster.id }, () => this.verification.enqueue(cluster.nodeId, { reason: 'cluster', clusterIds: [cluster.id], parentJobId }))); }
|
|
196
|
+
if (summaryId) jobs.push(this.once(digest(['reflect-summary', summaryId]), { kind: 'routine', summaryId }, () => this.verification.enqueue(nodeId, { reason: 'window', summaryId, parentJobId })));
|
|
197
|
+
const stagnation = this.metrics.stagnation(nodeId);
|
|
198
|
+
if (stagnation) jobs.push(this.once(digest(['reflect-stagnation', nodeId, stagnation.turnIds]), { kind: 'stagnation', ...stagnation }, () => this.verification.enqueue(nodeId, { reason: 'stagnation', parentJobId })));
|
|
199
|
+
const trend = this.metrics.compare(previousWindow, currentWindow);
|
|
200
|
+
if (trend.comparable && trend.notImproved.length) jobs.push(this.once(digest(['reflect-trend', previousWindow.id, currentWindow.id]), { kind: 'trend', previousWindow: previousWindow.id, currentWindow: currentWindow.id, trend }, () => this.verification.enqueue(nodeId, { reason: 'window', summaryId, parentJobId })));
|
|
201
|
+
return [...new Map(jobs.map(j => [j.id, j])).values()];
|
|
202
|
+
}
|
|
203
|
+
}
|