refactor(workflow): contract shared core (#33)
This commit is contained in:
@@ -7,7 +7,7 @@ const {
|
||||
validateCtePlanV2,
|
||||
validateCteResultThin,
|
||||
validatePhaseSummaryV2,
|
||||
} = require("../artifact-contracts.js");
|
||||
} = require("../core/artifact-contracts.js");
|
||||
|
||||
// --- validateCtePlanV2 (payload A) ------------------------------------------------
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const {
|
||||
buildSchemaLinkingRequest,
|
||||
buildJoinReviewRequest,
|
||||
withChildLinkage,
|
||||
} = require("../builders.js");
|
||||
} = require("../core/builders.js");
|
||||
|
||||
const GOLDEN = path.join(__dirname, "golden");
|
||||
const golden = (name) => JSON.parse(fs.readFileSync(path.join(GOLDEN, name)));
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
enrichCtePlanV2,
|
||||
buildCteResultV2,
|
||||
enrichPhaseSummaryV2,
|
||||
} = require("../enrich.js");
|
||||
} = require("../core/enrich.js");
|
||||
|
||||
// --- enrichCtePlanV2 --------------------------------------------------------------
|
||||
|
||||
@@ -202,7 +202,7 @@ test("enrichPhaseSummaryV2 preserves open_questions/checks untouched", () => {
|
||||
|
||||
// --- appendLedgerSection -----------------------------------------------------------
|
||||
|
||||
const { appendLedgerSection } = require("../enrich.js");
|
||||
const { appendLedgerSection } = require("../core/enrich.js");
|
||||
|
||||
test("appendLedgerSection appends only the phase's substantive decisions", () => {
|
||||
const data = { schema_version: 2, summary: "s", sections: [{ title: "Criteri", items: [] }] };
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { createMemoryGate } = require("../memory/index.js");
|
||||
const { buildMultiselectRequest } = require("../core/builders.js");
|
||||
const { isReserved } = require("../core/reserved-labels.mjs");
|
||||
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||
|
||||
|
||||
@@ -74,6 +76,10 @@ test("the public Memory facade owns F8 policy and mutation ordering", async () =
|
||||
return null;
|
||||
},
|
||||
},
|
||||
reviewer: {
|
||||
buildMultiselect: buildMultiselectRequest,
|
||||
isReserved,
|
||||
},
|
||||
waitForReviewer: async (runtimeContext, widget) => {
|
||||
const response = await runtimeContext.ui.input(JSON.stringify(widget), "");
|
||||
return JSON.parse(response);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { createMemoryGate } = require("../memory/index.js");
|
||||
const { buildMultiselectRequest } = require("../core/builders.js");
|
||||
const { isReserved } = require("../core/reserved-labels.mjs");
|
||||
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||
|
||||
|
||||
@@ -71,6 +73,10 @@ function setupRecall(choices) {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
reviewer: {
|
||||
buildMultiselect: buildMultiselectRequest,
|
||||
isReserved,
|
||||
},
|
||||
waitForReviewer: async (runtimeContext, widget) => {
|
||||
const response = await runtimeContext.ui.input(JSON.stringify(widget), "");
|
||||
return JSON.parse(response);
|
||||
|
||||
@@ -75,7 +75,7 @@ test("prepareReviewerArguments leaves a legacy markdown string artifact.data UNC
|
||||
|
||||
test("GATE_CODE_FILES blocks writes to gate extensions, not session artifacts", () => {
|
||||
assert.ok(GATE_CODE_FILES.test("harness/.pi/extensions/tht-gate.js"));
|
||||
assert.ok(GATE_CODE_FILES.test(".pi/extensions/reserved-labels.mjs"));
|
||||
assert.ok(GATE_CODE_FILES.test(".pi/extensions/gate/core/reserved-labels.mjs"));
|
||||
assert.equal(GATE_CODE_FILES.test("sessions/s1/schema_linking.json"), false);
|
||||
assert.equal(GATE_CODE_FILES.test("sessions/s1/sql_final.sql"), false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { readdirSync, readFileSync } = require("node:fs");
|
||||
const { dirname, extname, join, relative, resolve, sep } = require("node:path");
|
||||
|
||||
const gateRoot = resolve(__dirname, "..");
|
||||
const domainRoots = ["disambiguation", "memory"];
|
||||
|
||||
|
||||
function productionFiles(root) {
|
||||
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(root, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return entry.name === "__tests__" ? [] : productionFiles(path);
|
||||
}
|
||||
return [".js", ".mjs"].includes(extname(entry.name)) ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function relativeImports(path) {
|
||||
const source = readFileSync(path, "utf8");
|
||||
return [...source.matchAll(/(?:from\s+|import\s*(?:\(\s*)?|require\s*\(\s*)["'](\.[^"']+)["']/g)]
|
||||
.map((match) => match[1]);
|
||||
}
|
||||
|
||||
|
||||
test("gate domain modules receive shared behavior as capabilities", () => {
|
||||
for (const domain of domainRoots) {
|
||||
const root = join(gateRoot, domain);
|
||||
for (const path of productionFiles(root)) {
|
||||
const imports = relativeImports(path).map((specifier) =>
|
||||
relative(gateRoot, resolve(dirname(path), specifier)).split(sep).join("/"));
|
||||
const violations = imports.filter((target) =>
|
||||
target !== domain && !target.startsWith(`${domain}/`));
|
||||
assert.deepEqual(
|
||||
violations,
|
||||
[],
|
||||
`${relative(gateRoot, path)} imports gate implementation directly: ${violations.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { isReserved, stripReserved } = require("../../reserved-labels.mjs");
|
||||
const { isReserved, stripReserved } = require("../core/reserved-labels.mjs");
|
||||
|
||||
test("Altro variants (any punctuation/case) are reserved", () => {
|
||||
assert.equal(isReserved("Altro — specifica…"), true);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Etichette riservate del gate HITL e helper di deduplica. Modulo PURO (nessuna
|
||||
// dipendenza da pi-tui): condiviso da tht-gate.js e testabile in isolamento con
|
||||
// node --test .pi/extensions/test_reserved_labels.mjs
|
||||
// `Altro — specifica…` e le due vie di fuga (torna indietro / esci) vengono
|
||||
// SEMPRE aggiunte dal gate alle opzioni in arrivo: se il modello ri-propone uno
|
||||
// step con queste etichette gia' presenti, stripReserved evita il duplicato.
|
||||
|
||||
export const ALTRO = "Altro — specifica…";
|
||||
export const QUIT_LABEL = "Esci da Pi (/quit)";
|
||||
export const BACK_LABEL = "Torna indietro (fase precedente)";
|
||||
export const CONTROL_LABELS = new Set([QUIT_LABEL, BACK_LABEL]);
|
||||
|
||||
// Normalize a label to lowercase ASCII tokens: strip diacritics, turn every run
|
||||
// of punctuation/space into a single space, trim. "Altro — specifica…" -> "altro specifica".
|
||||
function normalize(label) {
|
||||
return String(label)
|
||||
.normalize("NFD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
const NORM_QUIT = normalize(QUIT_LABEL);
|
||||
const NORM_BACK = normalize(BACK_LABEL);
|
||||
|
||||
// true if the label is one the gate adds itself. Robust to the variants models
|
||||
// emit ("Altro - specificare", "altro", English "Other — specify"): reserved when
|
||||
// the FIRST normalized token is exactly "altro"/"other", or the whole normalized
|
||||
// label equals the canonical quit/back labels. First-token match keeps real
|
||||
// options like "altrove" out of the reserved set.
|
||||
export function isReserved(label) {
|
||||
const n = normalize(label);
|
||||
if (!n) return false;
|
||||
const first = n.split(" ")[0];
|
||||
return first === "altro" || first === "other" || n === NORM_QUIT || n === NORM_BACK;
|
||||
}
|
||||
|
||||
// removes every reserved entry from a list of labels, preserving order and normal
|
||||
// entries. Idempotent.
|
||||
export function stripReserved(labels) {
|
||||
return labels.filter((label) => !isReserved(label));
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
import { Type } from "typebox";
|
||||
|
||||
|
||||
export const NEW_SESSION_CLARIFICATION_KICKOFF =
|
||||
"2. Nel primo turno identifica la SOLA ambiguità con maggiore impatto sulla query e " +
|
||||
"chiama subito `reviewer_select` con opzioni concrete. Niente lunga narrazione, elenco " +
|
||||
"di tutte le ambiguità o ricapitolazione preliminare.\n";
|
||||
|
||||
|
||||
export const EMPTY_SESSION_CLARIFICATION_KICKOFF =
|
||||
"Se la sessione e' in fase 1 e non ha decisioni, dopo `session show` chiama " +
|
||||
"`reviewer_select` subito: non produrre testo libero.\n";
|
||||
|
||||
|
||||
function normalizedAssumptions(assumptions) {
|
||||
let normalized = assumptions;
|
||||
if (typeof normalized === "string") {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Type } from "typebox";
|
||||
|
||||
import { buildMultiselectRequest } from "../builders.js";
|
||||
import { isReserved } from "../../reserved-labels.mjs";
|
||||
|
||||
// Compatibility note: reviewer labels move verbatim from the composition root.
|
||||
// Tickets #22 and #23 require observable parity; translating existing chrome is a
|
||||
// separate product behavior change rather than part of these extractions.
|
||||
@@ -80,14 +77,14 @@ function shouldSkipEmptyRecall({ meritCount, allowEmpty, advance }) {
|
||||
|
||||
|
||||
async function reviewRecall(ctx, params, phase, dependencies) {
|
||||
const { workflow, ledger, waitForReviewer, toTextResult } = dependencies;
|
||||
const { workflow, ledger, reviewer, waitForReviewer, toTextResult } = dependencies;
|
||||
try {
|
||||
const { session, advance } = params;
|
||||
const options = normalizeMemoryOptions(params.options);
|
||||
const typeError = ledger.validate(ctx, options, session);
|
||||
if (typeError) return toTextResult(typeError);
|
||||
const meritOptions = options
|
||||
.filter((option) => !isReserved(option.label))
|
||||
.filter((option) => !reviewer.isReserved(option.label))
|
||||
.map((option) => ({
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
@@ -110,7 +107,7 @@ async function reviewRecall(ctx, params, phase, dependencies) {
|
||||
"avanzamento automatico alla fase successiva.",
|
||||
);
|
||||
}
|
||||
const widget = buildMultiselectRequest({
|
||||
const widget = reviewer.buildMultiselect({
|
||||
id: `u${Date.now()}`,
|
||||
phase,
|
||||
allowEmpty: params.allow_empty ?? false,
|
||||
@@ -225,7 +222,7 @@ function splitPromotionChoices(candidates, choices) {
|
||||
*/
|
||||
function installMemoryGate(
|
||||
pi,
|
||||
{ workflow, memory, ledger, waitForReviewer, toTextResult },
|
||||
{ workflow, memory, ledger, reviewer, waitForReviewer, toTextResult },
|
||||
) {
|
||||
pi.registerTool({
|
||||
name: "reviewer_memory_promote",
|
||||
@@ -285,7 +282,7 @@ function installMemoryGate(
|
||||
);
|
||||
}
|
||||
const options = promotionOptions(candidates);
|
||||
const widget = buildMultiselectRequest({
|
||||
const widget = reviewer.buildMultiselect({
|
||||
id: `u${Date.now()}`,
|
||||
phase: phase.id,
|
||||
title: "Quali concetti chiariti salvare nella memoria riutilizzabile?",
|
||||
|
||||
Reference in New Issue
Block a user