Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions docs/adr/0017-procedural-graph-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,26 @@ must separately honor current revocation. Keyverse/owner retains signer identity
key custody; graph publication, released cross-service contracts, canary/rollback and
product-owner outcome evidence remain outside this boundary.

Candidate #603 adds a publication-time preflight under Noema Policy / Approval. It
freshly reads the existing State / Checkpoint history and Policy / Approval ledger
twice, rejects movement inside that stable-read window, rejects a current revocation,
and requires the exact candidate/history/evaluator-handoff/signer/approval identity
to agree. Provenance assertions on both owner snapshots and the returned preflight
receipt reject structural clones. A successful receipt explicitly carries
`publicationAuthorized: false` and `activationAuthorized: false`; it is point-in-time
precondition evidence only and does not publish or activate a graph. A later publisher
must atomically/CAS-bind the exact receipt to its own operation plus immutable released
external graph-contract and signer-trust inputs. Existing execution lifecycle and
cancellation authority stay in Agent Runtime and Workflow / Task Execution rather than
becoming graph-publication truth. ADR-0017 remains `Proposed` while #603 is candidate
source and after source integration until release/deployment/shadow/canary evidence
satisfies the separate acceptance boundaries.

## CWL ownership and rollout

| Owner | Planned responsibility; not a claim of deployed integration |
| --- | --- |
| Noema | Graph snapshot, guidance context, offline screening, signed evaluator-handoff verification, workflow-backed current-state guidance ACL, bounded State / Checkpoint evaluation/rejection history, provenance-preserving history reads, and protected #601 Policy / Approval CAS; graph publication/activation composition remains separate work |
| Noema | Graph snapshot, guidance context, offline screening, signed evaluator-handoff verification, workflow-backed current-state guidance ACL, bounded State / Checkpoint evaluation/rejection history, provenance-preserving history reads, protected #601 Policy / Approval CAS, and candidate #603 publication-time preflight; graph publication/activation remains separate work |
| context-graph-contracts | Released language-neutral schemas, digest rules, conformance fixtures |
| enterprise-architecture-core | Capability/owner map, versioned adoption matrix and evidence classes |
| contextual-orchestrator | Existing gateway routing for later guide/solver/refiner calls; no client-side provider fallback |
Expand All @@ -178,19 +193,19 @@ product-owner outcome evidence remain outside this boundary.
| .github and product owners | Central development profile and product-specific procedural graphs/adapters/tests |

1. Keep the deterministic core, workflow-backed current-state ACL, authenticated
evaluator handoff, durable evidence history, and Policy / Approval ledger advisory/evidence-only;
none grants activation.
evaluator handoff, durable evidence history, Policy / Approval ledger, and candidate
publication preflight advisory/evidence-only; none grants activation.
2. Have contract/EAC owners release interoperable schemas and ownership records.
Do not consume mutable sibling PR heads or independently copy this runtime.
3. Integrate read-only shadow guidance through the existing orchestrator boundary
in the central development loop and Naruon. Compare no graph, fixed graph, and
evolved graph under matched conditions. Measure task success, sequence errors,
duplicate effects, tokens/cost, and latency separately; do not invent gains.
4. Reuse protected signed evaluator verification, State / Checkpoint durable rejection
history, verified snapshot provenance, and #601 Policy / Approval CAS as prerequisites;
add sanitized trajectory extraction, offline candidate generation, publication-time
fresh cross-authority reconciliation, graph publication and recovery without creating
duplicate truth.
history, verified snapshot provenance, and #601 Policy / Approval CAS as prerequisites.
Candidate #603 adds fresh cross-authority reconciliation; sanitized trajectory
extraction, offline candidate generation, actual graph publication and recovery
remain separate work and must not create duplicate truth.
5. Enable opt-in canaries for other products only after their own conformance and
rollback evidence. Accounting postings, billing, employment assessment, data
deletion and deployment retain their independent high-risk approval controls.
Expand All @@ -216,16 +231,17 @@ composition plus failure/restart and buyer-path latency evidence; the p95 <=20 m
target is measured against the deployed path rather than inferred from unit tests.

There is still no production graph publication/trajectory store, automatic refiner,
product invocation, or publication/activation composition that freshly reconciles
current State / Checkpoint and Policy / Approval authority. Protected #594 provides
signed evaluator-handoff verification, #597 provides bounded durable evaluation/
rejection history, #599 provides repository-verified read provenance, and #601 provides
the Policy / Approval CAS ledger; none of those source slices is release, deployment,
graph publication, or activation authority. There is also no evidence yet that graph
guidance improves CWL tasks. The owning root product/technical baseline must retain
these gaps without replacing historical results. Do not mark ADR-0017 Accepted,
publish a release, or advertise organization-wide activation from source integration
or tracking issues.
product invocation, or activation composition. Candidate #603 adds the missing
publication-time State / Checkpoint + Policy / Approval reconciliation preflight, but
that preflight is deliberately not an atomic publisher and carries no publication or
activation authority. Protected #594 provides signed evaluator-handoff verification,
#597 provides bounded durable evaluation/rejection history, #599 provides repository-
verified read provenance, and #601 provides the Policy / Approval CAS ledger; none of
those source slices, nor candidate #603, is release, deployment, graph publication,
or activation authority. There is also no evidence yet that graph guidance improves
CWL tasks. The owning root product/technical baseline must retain these gaps without
replacing historical results. Do not mark ADR-0017 Accepted, publish a release, or
advertise organization-wide activation from source integration or tracking issues.

## References

Expand Down
276 changes: 276 additions & 0 deletions src/policy-approval/procedural-publication-preflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import {
assertProceduralGraph,
type ProceduralGraph,
} from "../agent-runtime/procedural-graph";
import type {
ProceduralEvaluationHistorySnapshot,
} from "../state-checkpoint/procedural-evaluation-history";
import {
assertProceduralEvaluationHistorySnapshot,
} from "../state-checkpoint/procedural-evaluation-history-authority";
import {
assertProceduralPolicyApprovalSnapshot,
type ProceduralPolicyApprovalSnapshot,
} from "./procedural-policy-approval";

const PREFLIGHT_SCHEMA_VERSION = "noema.procedural-publication-preflight/v1" as const;
const admittedPreflights = new WeakSet<object>();

/**
* State / Checkpoint read port used only to obtain current durable procedural evaluation history.
* Implementations retain storage ownership; this composition boundary never writes that state.
*/
export interface ProceduralPublicationHistoryReader {
/**
* Reads the current verified State / Checkpoint snapshot for the exact admitted graph lineage.
* @param candidate Locally admitted procedural graph whose lineage selects the current history.
* @returns A provenance-bearing current snapshot, or null when no durable history exists.
*/
read(candidate: ProceduralGraph): Promise<ProceduralEvaluationHistorySnapshot | null>;
}

/**
* Policy / Approval read port used only to obtain current durable procedural approval state.
* Implementations retain policy ownership; this composition boundary never mints a decision.
*/
export interface ProceduralPublicationApprovalReader {
/**
* Reads the current verified Policy / Approval snapshot for the exact admitted graph lineage.
* @param candidate Locally admitted procedural graph whose lineage selects the approval ledger.
* @returns A provenance-bearing current snapshot, or null when no approval state exists.
*/
read(candidate: ProceduralGraph): Promise<ProceduralPolicyApprovalSnapshot | null>;
}

/** Stable diagnostic codes for publication-preflight reconciliation failures; none grants retry authority. */
export type ProceduralPublicationPreflightErrorCode =
| "history_unavailable"
| "approval_unavailable"
| "authority_changed_during_reconciliation"
| "approval_revoked"
| "approval_does_not_match_current_history"
| "unadmitted_preflight";

/**
* Fail-closed error emitted when current owner evidence cannot establish one stable publication precondition.
* The error is diagnostic only and cannot publish, activate, approve, retry, or mutate a procedural graph.
*/
export class ProceduralPublicationPreflightError extends Error {
readonly code: ProceduralPublicationPreflightErrorCode;

/**
* Creates one stable failure classification without carrying owner secrets or mutable evidence payloads.
* @param code Canonical failure class produced by the reconciliation boundary.
*/
constructor(code: ProceduralPublicationPreflightErrorCode) {
super(code);
this.name = "ProceduralPublicationPreflightError";
this.code = code;
}
}

/**
* Point-in-time evidence that current State / Checkpoint and Policy / Approval snapshots agreed across
* one stable-read window. It is deliberately not graph publication, deployment, or activation authority.
*/
export interface ProceduralPublicationPreflightReceipt {
readonly schemaVersion: typeof PREFLIGHT_SCHEMA_VERSION;
readonly tenantId: string;
readonly taskType: string;
readonly graphId: string;
readonly candidateRevision: number;
readonly candidateDigest: string;
readonly historyVersion: number;
readonly historyHeadEventDigest: string;
readonly evaluationEnvelopeDigest: string;
readonly evaluatorHandoffDigest: string;
readonly signerKeyId: string;
readonly approvalVersion: number;
readonly approvalHeadEventDigest: string;
readonly approvalDecisionId: string;
readonly policyVersion: string;
readonly approvalStatus: "approved_for_pilot";
readonly publicationAuthorized: false;
readonly activationAuthorized: false;
}

function rejectPreflight(code: ProceduralPublicationPreflightErrorCode): never {
throw new ProceduralPublicationPreflightError(code);
}

async function currentHistory(
reader: ProceduralPublicationHistoryReader,
candidate: ProceduralGraph,
): Promise<ProceduralEvaluationHistorySnapshot> {
const snapshot = await reader.read(candidate);
if (snapshot === null) rejectPreflight("history_unavailable");
assertProceduralEvaluationHistorySnapshot(snapshot);
return snapshot;
}

async function currentApproval(
reader: ProceduralPublicationApprovalReader,
candidate: ProceduralGraph,
): Promise<ProceduralPolicyApprovalSnapshot> {
const snapshot = await reader.read(candidate);
if (snapshot === null) rejectPreflight("approval_unavailable");
assertProceduralPolicyApprovalSnapshot(snapshot);
return snapshot;
}

function sameHistoryPosition(
before: ProceduralEvaluationHistorySnapshot,
after: ProceduralEvaluationHistorySnapshot,
): boolean {
return JSON.stringify([
before.stream.tenantId,
before.stream.taskType,
before.stream.graphId,
before.version,
before.headEventDigest,
]) === JSON.stringify([
after.stream.tenantId,
after.stream.taskType,
after.stream.graphId,
after.version,
after.headEventDigest,
]);
}

function sameApprovalPosition(
before: ProceduralPolicyApprovalSnapshot,
after: ProceduralPolicyApprovalSnapshot,
): boolean {
return JSON.stringify([
before.tenantId,
before.taskType,
before.graphId,
before.version,
before.status,
before.candidateDigest,
before.historyVersion,
before.historyHeadEventDigest,
before.policyVersion,
before.headEventDigest,
]) === JSON.stringify([
after.tenantId,
after.taskType,
after.graphId,
after.version,
after.status,
after.candidateDigest,
after.historyVersion,
after.historyHeadEventDigest,
after.policyVersion,
after.headEventDigest,
]);
}

function approvalMatchesCurrentHistory(
candidate: ProceduralGraph,
history: ProceduralEvaluationHistorySnapshot,
approval: ProceduralPolicyApprovalSnapshot,
): boolean {
const evaluation = history.events.at(-1)!;
const approvalEvent = approval.events.at(-1)!;
return JSON.stringify([
approval.tenantId,
approval.taskType,
approval.graphId,
approval.candidateDigest,
approval.historyVersion,
approval.historyHeadEventDigest,
approvalEvent.envelopeDigest,
approvalEvent.handoffDigest,
approvalEvent.signerKeyId,
]) === JSON.stringify([
candidate.tenantId,
candidate.taskType,
candidate.graphId,
candidate.digest,
history.version,
history.headEventDigest,
evaluation.envelopeDigest,
evaluation.handoffDigest,
evaluation.signerKeyId,
]);
}

/**
* Policy / Approval anti-corruption boundary that reconciles existing owner snapshots immediately before
* a later publisher acts. It performs no publication itself and introduces no new durable source of truth.
*/
export class ProceduralPublicationPreflight {
constructor(
private readonly historyReader: ProceduralPublicationHistoryReader,
private readonly approvalReader: ProceduralPublicationApprovalReader,
) {}

/**
* Reads current State / Checkpoint and Policy / Approval twice, rejecting concurrent drift, revocation,
* or exact identity mismatch. The successful receipt is only a point-in-time precondition: authority can
* change after return, so an actual publisher must bind the exact receipt to its own atomic/CAS operation
* and to released external contract/trust inputs. Existing execution lifecycle and cancellation checks stay
* in Agent Runtime/Workflow boundaries rather than being duplicated as graph-publication truth here.
* @param candidate Locally admitted candidate graph proposed for a later publication operation.
* @returns A locally admitted immutable receipt that explicitly carries no publication or activation authority.
*/
async reconcile(candidate: ProceduralGraph): Promise<ProceduralPublicationPreflightReceipt> {
assertProceduralGraph(candidate);

const historyBefore = await currentHistory(this.historyReader, candidate);
const approvalBefore = await currentApproval(this.approvalReader, candidate);
const historyAfter = await currentHistory(this.historyReader, candidate);
if (!sameHistoryPosition(historyBefore, historyAfter)) {
rejectPreflight("authority_changed_during_reconciliation");
}

const approvalAfter = await currentApproval(this.approvalReader, candidate);
if (!sameApprovalPosition(approvalBefore, approvalAfter)) {
rejectPreflight("authority_changed_during_reconciliation");
}
if (approvalAfter.status === "revoked") rejectPreflight("approval_revoked");
if (!approvalMatchesCurrentHistory(candidate, historyAfter, approvalAfter)) {
rejectPreflight("approval_does_not_match_current_history");
}

const evaluation = historyAfter.events.at(-1)!;
const approvalEvent = approvalAfter.events.at(-1)!;
const receipt: ProceduralPublicationPreflightReceipt = Object.freeze({
schemaVersion: PREFLIGHT_SCHEMA_VERSION,
tenantId: candidate.tenantId,
taskType: candidate.taskType,
graphId: candidate.graphId,
candidateRevision: candidate.revision,
candidateDigest: candidate.digest,
historyVersion: historyAfter.version,
historyHeadEventDigest: historyAfter.headEventDigest,
evaluationEnvelopeDigest: evaluation.envelopeDigest,
evaluatorHandoffDigest: evaluation.handoffDigest,
signerKeyId: evaluation.signerKeyId,
approvalVersion: approvalAfter.version,
approvalHeadEventDigest: approvalAfter.headEventDigest,
approvalDecisionId: approvalEvent.decisionId,
policyVersion: approvalAfter.policyVersion,
approvalStatus: "approved_for_pilot",
publicationAuthorized: false,
activationAuthorized: false,
});
admittedPreflights.add(receipt);
return receipt;
}
}

/**
* Requires process-local provenance from `ProceduralPublicationPreflight.reconcile`; serialized or
* caller-constructed lookalikes cannot be promoted into a downstream publication precondition.
* @param value Unknown candidate receipt proposed to a later graph-publication adapter.
* @returns Nothing; normal return means this process emitted the receipt after current-owner reconciliation.
*/
export function assertProceduralPublicationPreflight(
value: unknown,
): asserts value is ProceduralPublicationPreflightReceipt {
if (!admittedPreflights.has(value as object)) {
throw new ProceduralPublicationPreflightError("unadmitted_preflight");
}
}
14 changes: 14 additions & 0 deletions test/procedural-publication-preflight-documentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";

const adr = readFileSync("docs/adr/0017-procedural-graph-guidance.md", "utf8");

describe("procedural publication preflight documentation authority", () => {
it("classifies #603 as candidate preflight rather than publication or activation authority", () => {
expect(adr).toContain("Candidate #603 adds a publication-time preflight");
expect(adr).toContain("publicationAuthorized: false");
expect(adr).toContain("activationAuthorized: false");
expect(adr).toContain("does not publish or activate a graph");
expect(adr).toContain("ADR-0017 remains `Proposed`");
});
});
Loading
Loading