Skip to content

Commit f20edbc

Browse files
neo-opus-adatobiuAda (Claude Opus 4.8)
authored
feat(graph): convergence-snapshot canonical-id schema + firewalled render-target (#14633) (#14679)
* feat(graph): convergence-snapshot canonical-id schema + firewalled render-target (#14633) Leaf 1 (foundation) of epic #14581 (Convergence-weighted Golden Path). Defines the CONVERGENCE_SNAPSHOT node schema keyed on CANONICAL concept ids (reuses the shipped normalizeConceptKey policy, never re-derived) recording id provenance (OQ1); resolves the render-target to a notAuthority, non-boot-consumable terrain ledger (OQ8 self-fulfilling firewall); binds the shipped four-axis CONTRACT_AXES kept SEPARATE, never flattened; carries the risk-node flag + the born-scheduled longitudinal falsifier (remeasureAt). convergenceWeight + independenceBudget are declared-but-null (Leaf 2 #14634 fills them; the builder never invents a weight). ADR-0024 disposition: additive/fail-open + DECAYING (re-derivable read, not durable authority) — excluded from PROTECTED_EDGE_TYPES. EVOLUTION_GOAL binding is an explicit unresolved stub (#14565 / PR #14626 unmerged), reconciled when it lands. 7 unit tests green. Co-Authored-By: Ada (Claude Opus 4.8) <ada@neomjs.dev> * docs(adr): register CONVERGENCE_SNAPSHOT node class in ADR-0024 §2.2 (#14633) Addresses the cross-family review RC (Euclid): a new node class must be registered in ADR-0024's model ledger, not only in the module JSDoc. Adds an ADR-0024 §2.2 amendment note (mirroring the ADR-0033 EVOLUTION_GOAL precedent) registering CONVERGENCE_SNAPSHOT + its disposition: additive, fail-open, re-derivable, render-only/human-facing, NOT durable authority, node-side non-protected (DECAYING). Node writes + the post-sync integrity canary land with the compute leaf; this leaf is schema-only. Also fixes the module JSDoc's imprecise 'excluded from PROTECTED_EDGE_TYPES' — that enum governs EDGE facts, not node-class membership; the two are orthogonal (per the RC's wording ask). Co-Authored-By: Ada (Claude Opus 4.8) <ada@neomjs.dev> --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com> Co-authored-by: Ada (Claude Opus 4.8) <ada@neomjs.dev>
1 parent 44c7399 commit f20edbc

3 files changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {normalizeConceptKey} from './conceptSpineCanonicalization.mjs';
2+
import {CONTRACT_AXES} from './conceptNeighborhoodProbe.mjs';
3+
4+
/**
5+
* @module ai/services/graph/convergenceSnapshotSchema
6+
* @summary Convergence-weighted Golden Path — canonical-id snapshot node schema + firewalled render-target (#14633; ticket-ref-ok: owning-leaf anchor).
7+
*
8+
* Foundation leaf of epic #14581 (ticket-ref-ok: parent-epic anchor). A convergence snapshot annotates a
9+
* goal→sub-goal lattice node with how invariant it is across imagined futures ("structure not events" —
10+
* the sub-goals worth doing regardless of which future wins). This module defines ONLY the node schema
11+
* plus the two firewall-critical resolutions; the compute (Leaf 2) and the human render-ledger
12+
* (Leaf 3) build on it.
13+
*
14+
* Two hazards are closed at the schema layer:
15+
* - **OQ1 node-identity drift:** every snapshot keys on a CANONICAL concept id (via the shipped
16+
* `normalizeConceptKey` policy — reused, never re-derived) and records that id's provenance.
17+
* - **OQ8 self-fulfilling firewall:** the render-target resolves to a surface NO agent boot-path
18+
* consumes (`resolveRenderTarget`) — decision-support for humans, invisible to future-generation.
19+
*
20+
* Four-axis contract: annotations bind the shipped `CONTRACT_AXES` (authority / fidelity /
21+
* extractionProvenance / lifecycle) from the concept-graph measurement floor — the four axes stay
22+
* SEPARATE, never flattened to a composite score.
23+
*
24+
* ADR-0024 disposition (native-edge-graph-model; ticket-ref-ok: leaf-AC decay anchor; registered in ADR-0024 §2.2):
25+
* the `CONVERGENCE_SNAPSHOT` node class is ADDITIVE, FAIL-OPEN, re-derivable, and render-only / human-facing —
26+
* a read over the goal→sub-goal lattice, NOT durable authority. It is therefore **node-side non-protected
27+
* (DECAYING)**: a snapshot stale past its `remeasureAt` is discarded and recomputed, never trusted.
28+
* (`PROTECTED_EDGE_TYPES` governs EDGE facts, not node-class membership — the two are orthogonal.)
29+
*
30+
* EVOLUTION_GOAL binding: the shared `EVOLUTION_GOAL` schema (from the sibling direction chain) is
31+
* unmerged at this leaf's authoring, so the snapshot references it through `EVOLUTION_GOAL_SCHEMA_REF`
32+
* — a stub bound to the epic contract, reconciled to the real export when the sibling chain lands.
33+
*/
34+
35+
export const CONVERGENCE_SNAPSHOT_NODE_TYPE = 'CONVERGENCE_SNAPSHOT';
36+
37+
/**
38+
* Stub reference to the shared `EVOLUTION_GOAL` schema (#14565 / PR #14626, unmerged; ticket-ref-ok: cross-lane dependency anchor).
39+
* The snapshot only needs the goal-id contract, which the epic fixes; reconcile `resolved` + a real
40+
* import when the sibling chain merges.
41+
*/
42+
export const EVOLUTION_GOAL_SCHEMA_REF = Object.freeze({
43+
ref : 'EVOLUTION_GOAL',
44+
resolved: false,
45+
contract: 'canonical goal/sub-goal id + declared-intent axis (shared with the #14565 direction chain)'
46+
});
47+
48+
/**
49+
* @summary The firewalled render-target for convergence output (OQ8). Resolves to a standalone ledger
50+
* artifact that NO agent boot-path consumes — the long-run home is the FM-cockpit terrain panel (a
51+
* surface agents never boot from). The redaction-filter branch is the reserved fallback, documented
52+
* not built.
53+
* @returns {Object} frozen `{target, notAuthority, agentBootConsumable, home, fallback}`.
54+
*/
55+
export function resolveRenderTarget() {
56+
return Object.freeze({
57+
target : 'convergence-terrain-ledger',
58+
notAuthority : true,
59+
agentBootConsumable: false,
60+
home : 'fm-cockpit-terrain-panel',
61+
fallback : 'redaction-filter'
62+
});
63+
}
64+
65+
/**
66+
* @summary Projects an axis payload onto the four-axis contract, keeping the axes SEPARATE (never a
67+
* composite score). Unknown axis keys are dropped; only the four `CONTRACT_AXES` names are retained.
68+
* @param {Object} [axes] Caller axis payload keyed by axis name.
69+
* @returns {Object} present axes only, among `{authority, fidelity, extractionProvenance, lifecycle}`.
70+
*/
71+
export function pickContractAxes(axes = {}) {
72+
const out = {};
73+
74+
for (const axis of Object.keys(CONTRACT_AXES)) {
75+
if (axes && axes[axis] !== undefined) {
76+
out[axis] = axes[axis];
77+
}
78+
}
79+
80+
return out;
81+
}
82+
83+
/**
84+
* @summary Builds a convergence-snapshot node keyed on the CANONICAL concept id of the lattice node it
85+
* annotates. Records id provenance (OQ1 — recorded, never invented), the four-axis annotations (never
86+
* flattened), the risk-node flag, and the born-scheduled longitudinal falsifier (`remeasureAt`). The
87+
* `convergenceWeight` + `independenceBudget` fields are declared but left `null` — the Leaf 2 compute
88+
* fills them; this builder never invents a weight.
89+
*
90+
* @param {Object} input
91+
* @param {String} input.latticeNodeId Raw goal/sub-goal id to annotate (canonicalized here).
92+
* @param {String} input.provenance How the canonical id was derived (OQ1 — recorded verbatim).
93+
* @param {Boolean} [input.riskNode=false] Risk-node annotation.
94+
* @param {String} [input.remeasureAt] ISO time of the born-scheduled longitudinal re-measurement.
95+
* @param {Object} [input.axes] Four-axis annotations keyed by `CONTRACT_AXES` axis name.
96+
* @param {String} [input.now] ISO clock injection for deterministic tests.
97+
* @returns {Object|null} the snapshot node record, or `null` when the id does not canonicalize.
98+
*/
99+
export function buildConvergenceSnapshotNode({latticeNodeId, provenance, riskNode = false, remeasureAt, axes, now} = {}) {
100+
const canonicalId = normalizeConceptKey(latticeNodeId);
101+
102+
if (!canonicalId) return null;
103+
104+
return {
105+
type : CONVERGENCE_SNAPSHOT_NODE_TYPE,
106+
id : `${CONVERGENCE_SNAPSHOT_NODE_TYPE}:${canonicalId}`,
107+
properties: {
108+
canonicalId,
109+
provenance : provenance || null,
110+
riskNode : riskNode === true,
111+
remeasureAt : remeasureAt || null,
112+
convergenceWeight : null,
113+
independenceBudget: null,
114+
axes : pickContractAxes(axes),
115+
renderTarget : resolveRenderTarget().target,
116+
evolutionGoalRef : EVOLUTION_GOAL_SCHEMA_REF.ref,
117+
createdAt : now || null
118+
}
119+
};
120+
}

learn/agentos/decisions/0024-native-edge-graph-model.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ Four named enums (`CONCEPT_EDGE_TYPES`, `ADR_EDGE_TYPES`, `PROTECTED_EDGE_TYPES`
6666
> PR per this record's own re-review trigger; new node classes ship with the post-sync integrity
6767
> canary.
6868
69+
> **Amended by #14633 (Convergence-weighted GP, Leaf 1 of #14581):** registers the `CONVERGENCE_SNAPSHOT`
70+
> node class (`ai/services/graph/convergenceSnapshotSchema.mjs`) — the convergence-terrain sibling of the
71+
> ADR-0033 `EVOLUTION_GOAL` chain. Disposition: **additive, fail-open, re-derivable** (a read over the
72+
> goal→sub-goal lattice, not durable authority) and **render-only / human-facing** (its render-target is a
73+
> `notAuthority` terrain ledger no agent boot-path consumes). It is therefore **node-side non-protected —
74+
> DECAYING**: a snapshot stale past its `remeasureAt` is discarded and recomputed, never trusted. This is a
75+
> NODE-class disposition; `PROTECTED_EDGE_TYPES` (§2.3) governs EDGE facts, not node-class membership — the
76+
> two are orthogonal. Node writes + the post-sync integrity canary land with the compute leaf (#14634); this
77+
> leaf defines the schema only.
78+
6979
### 2.4 Topology — how it connects
7080

7181
```mermaid
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {test, expect} from '@playwright/test';
2+
3+
import {
4+
CONVERGENCE_SNAPSHOT_NODE_TYPE,
5+
EVOLUTION_GOAL_SCHEMA_REF,
6+
buildConvergenceSnapshotNode,
7+
pickContractAxes,
8+
resolveRenderTarget
9+
} from '../../../../../../ai/services/graph/convergenceSnapshotSchema.mjs';
10+
11+
test.describe('convergenceSnapshotSchema', () => {
12+
test('keys the snapshot on the CANONICAL concept id + records provenance, never inventing a weight (OQ1)', () => {
13+
const node = buildConvergenceSnapshotNode({
14+
latticeNodeId: 'CONCEPT:GoldenPath',
15+
provenance : 'lattice-import:2026-07-04',
16+
now : '2026-07-04T00:00:00.000Z'
17+
});
18+
19+
expect(node.type).toBe('CONVERGENCE_SNAPSHOT');
20+
expect(node.id).toBe('CONVERGENCE_SNAPSHOT:golden-path');
21+
expect(node.properties.canonicalId).toBe('golden-path');
22+
expect(node.properties.provenance).toBe('lattice-import:2026-07-04');
23+
// Leaf 2 compute fills these — the schema builder must never invent a weight.
24+
expect(node.properties.convergenceWeight).toBeNull();
25+
expect(node.properties.independenceBudget).toBeNull();
26+
expect(node.properties.createdAt).toBe('2026-07-04T00:00:00.000Z');
27+
});
28+
29+
test('carries the risk-node flag + the born-scheduled longitudinal falsifier', () => {
30+
const node = buildConvergenceSnapshotNode({
31+
latticeNodeId: 'first-revenue',
32+
provenance : 'p',
33+
riskNode : true,
34+
remeasureAt : '2026-08-04T00:00:00.000Z'
35+
});
36+
37+
expect(node.properties.riskNode).toBe(true);
38+
expect(node.properties.remeasureAt).toBe('2026-08-04T00:00:00.000Z');
39+
});
40+
41+
test('defaults are honest: no risk, no schedule, null provenance — nothing fabricated', () => {
42+
const node = buildConvergenceSnapshotNode({latticeNodeId: 'x'});
43+
44+
expect(node.properties.riskNode).toBe(false);
45+
expect(node.properties.remeasureAt).toBeNull();
46+
expect(node.properties.provenance).toBeNull();
47+
});
48+
49+
test('keeps the four axes SEPARATE — never flattened, unknown axes dropped', () => {
50+
const axes = pickContractAxes({
51+
authority : {trustTier: 'system'},
52+
fidelity : {sourceTier: 'curated'},
53+
extractionProvenance: {curated: true},
54+
lifecycle : {state: 'candidate'},
55+
composite : 0.87 // a flattened score must NOT survive
56+
});
57+
58+
expect(Object.keys(axes).sort()).toEqual(['authority', 'extractionProvenance', 'fidelity', 'lifecycle']);
59+
expect(axes).not.toHaveProperty('composite');
60+
expect(axes.lifecycle).toEqual({state: 'candidate'});
61+
});
62+
63+
test('render-target is firewalled (OQ8): notAuthority, NOT agent-boot-consumable, FM-cockpit home', () => {
64+
const target = resolveRenderTarget();
65+
66+
expect(target.target).toBe('convergence-terrain-ledger');
67+
expect(target.notAuthority).toBe(true);
68+
expect(target.agentBootConsumable).toBe(false);
69+
expect(target.home).toBe('fm-cockpit-terrain-panel');
70+
expect(target.fallback).toBe('redaction-filter');
71+
// The built node's render-target must be the firewalled ledger, not a boot-path surface.
72+
expect(buildConvergenceSnapshotNode({latticeNodeId: 'y'}).properties.renderTarget).toBe('convergence-terrain-ledger');
73+
});
74+
75+
test('returns null when the lattice id does not canonicalize (no phantom snapshot)', () => {
76+
expect(buildConvergenceSnapshotNode({latticeNodeId: ''})).toBeNull();
77+
expect(buildConvergenceSnapshotNode({latticeNodeId: '###'})).toBeNull();
78+
expect(buildConvergenceSnapshotNode({})).toBeNull();
79+
});
80+
81+
test('EVOLUTION_GOAL binding is an explicit unresolved stub (#14626 unmerged), reconciled later', () => {
82+
expect(EVOLUTION_GOAL_SCHEMA_REF.ref).toBe('EVOLUTION_GOAL');
83+
expect(EVOLUTION_GOAL_SCHEMA_REF.resolved).toBe(false);
84+
expect(buildConvergenceSnapshotNode({latticeNodeId: 'z'}).properties.evolutionGoalRef).toBe('EVOLUTION_GOAL');
85+
});
86+
});

0 commit comments

Comments
 (0)