From 5216f23aebd5db3395b558e760d10c5900912a18 Mon Sep 17 00:00:00 2001 From: EauDoon <47585778+EauDoon@users.noreply.github.com> Date: Sun, 30 Aug 2026 04:33:00 +0800 Subject: [PATCH] docs: align README human output with printHuman fields The pass-path sample omitted prove_scenario, prove_triggered_by, and the run-bundle path that the CLI always prints. Add JSDoc for the report shape and lock the README sample against printHuman. --- README.md | 3 ++ bin/aas.mjs | 100 ++++++++++++++++++++++++++++++++++++++++++++ test/stack.test.mjs | 24 +++++++++++ 3 files changed, 127 insertions(+) diff --git a/README.md b/README.md index ee5c929..2a1b238 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,10 @@ act_outcome: settled act_state: CLOSED act_fault: none prove: skipped +prove_scenario: none +prove_triggered_by: none flow: decide -> act +bundle: .out/runs/ ``` Fail closed at decide: diff --git a/bin/aas.mjs b/bin/aas.mjs index 3dbe3ac..c80fbd0 100644 --- a/bin/aas.mjs +++ b/bin/aas.mjs @@ -26,6 +26,66 @@ import { loadComponentLock, } from "../scripts/bootstrap.mjs"; +/** + * @typedef {object} ChildResult + * @property {number} status + * @property {string} stdout + * @property {string} stderr + * @property {Error} [error] + * + * @typedef {object} ComponentProvenance + * @property {string} name + * @property {string} repository + * @property {string} commit + * @property {string} [origin] + * @property {boolean} [detached] + * @property {boolean} [clean] + * @property {string[]} [entrypoints] + * + * @typedef {"pending"|"passed"|"failed"|"skipped"|"error"} StageStatus + * + * @typedef {object} DecideStage + * @property {StageStatus} status + * @property {boolean} [passed] + * @property {string|null} [policy_id] + * @property {unknown} [rule_results] + * @property {unknown} [error] + * @property {string} [reason] + * + * @typedef {object} ActStage + * @property {StageStatus} status + * @property {string|null} [outcome] + * @property {string|null} [state] + * @property {string|null} [fault] + * @property {string|null} [action_id] + * @property {unknown} [assurance_mode] + * @property {unknown} [bundle_verification] + * @property {string} [reason] + * + * @typedef {object} ProveStage + * @property {StageStatus} status + * @property {string} [scenario] + * @property {string} [triggered_by] + * @property {boolean} [ok] + * @property {string[]} [result_keys] + * @property {string} [reason] + * + * @typedef {object} RunReport + * @property {"agent-action-stack"} stack + * @property {"pass"|"fail"} response + * @property {string} flow + * @property {string} run_id + * @property {ComponentProvenance[]} component_provenance + * @property {{decide: DecideStage, act: ActStage, prove: ProveStage}} stages + * + * @typedef {object} DemoResult + * @property {RunReport} report + * @property {object} manifest + * @property {string} bundleDir + * @property {number} exitCode + * @property {boolean} asJson + */ + const root = join(dirname(fileURLToPath(import.meta.url)), ".."); export const DEFAULT_PATHS = Object.freeze({ root, @@ -85,6 +145,7 @@ the most recent complete bundle. `); } +/** @returns {ChildResult} */ export function runCapture(command, args, opts = {}) { const result = spawnSync(command, args, { encoding: "utf8", @@ -107,6 +168,13 @@ function childProcessError(label, result) { return new Error(`${label} child process exited with status ${result.status}`); } +/** + * Parse the last complete JSON value from mixed child stdout. + * + * @param {string} text + * @param {string} label Stage name used in error messages. + * @returns {unknown} + */ export function parseJsonOutput(text, label) { const trimmed = text.trim(); if (!trimmed) { @@ -157,6 +225,7 @@ function pythonCandidates() { return [["python3", []], ["python", []]]; } +/** @returns {ComponentProvenance[]} */ export function resolveComponentProvenance( depsDir = DEFAULT_PATHS.deps, lockPath = DEFAULT_PATHS.lock, @@ -186,6 +255,11 @@ export function resolveComponentProvenance( }); } +/** + * Evaluate a response fixture against the locked testbench policy. + * + * @returns {{ok: boolean, raw: object, status: number}} + */ export function runDecide( responsePath, { depsDir = DEFAULT_PATHS.deps, fixturesDir = DEFAULT_PATHS.fixtures, runner = runCapture } = {}, @@ -213,6 +287,12 @@ export function runDecide( throw new Error(`Python not found for decide stage${lastError ? ` (${lastError.code ?? "spawn-error"})` : ""}`); } +/** + * Execute the locked Consequence Rail refund demo. + * + * @param {string} fault Demo fault name, or "none". + * @returns {{ok: true, raw: object, status: number}} + */ export function runAct( fault, { depsDir = DEFAULT_PATHS.deps, runner = runCapture } = {}, @@ -232,6 +312,12 @@ export function runAct( return { ok: true, raw: payload, status: 0 }; } +/** + * Run MandateBound's operator simulation as the prove stage. + * + * @param {string} scenario MandateBound simulate scenario id. + * @returns {{ok: boolean, raw: object, status: number}} + */ export function runProve( scenario, { depsDir = DEFAULT_PATHS.deps, runner = runCapture } = {}, @@ -343,6 +429,13 @@ function stageRecord(status, fields = {}) { return { status, ...fields }; } +/** + * Print the CLI human report. Field order is part of the public surface and + * must match the pass-path sample in README.md. + * + * @param {RunReport} report + * @param {string|null} [bundleDir] + */ export function printHuman(report, bundleDir = null) { const lines = [ "stack: agent-action-stack", @@ -362,6 +455,13 @@ export function printHuman(report, bundleDir = null) { process.stdout.write(`${lines.join("\n")}\n`); } +/** + * Run decide → act → prove and persist an isolated bundle. + * + * @param {string[]} [args] Demo flags: --response, --fault, --dispute, --json. + * @param {object} [options] + * @returns {Promise} + */ export async function runDemo(args = [], options = {}) { validateDemoArgs(args); const paths = { diff --git a/test/stack.test.mjs b/test/stack.test.mjs index 37f1a13..7e14a85 100644 --- a/test/stack.test.mjs +++ b/test/stack.test.mjs @@ -8,6 +8,7 @@ import { loadComponentLock, inspectDependencyDirectory, npmInvocation } from ".. import { parseJsonOutput, persistRunBundle, + printHuman, resolveComponentProvenance, runAct, runDecide, @@ -217,6 +218,29 @@ test("child output parser accepts JSON before a trailing log", () => { ); }); +test("README pass-path sample matches printHuman field order", async () => { + const readme = readFileSync(join(ROOT, "README.md"), "utf8"); + const match = readme.match(/Expected human output \(pass path, no fault\):\n\n```text\n([\s\S]*?)```/); + assert.ok(match, "README is missing the pass-path human output sample"); + const outputRoot = tempRoot(); + const result = await runDemo(["--response", "pass"], stubOptions(outputRoot, { runId: "pass-run" })); + const chunks = []; + const originalWrite = process.stdout.write; + process.stdout.write = (chunk, encoding, callback) => { + chunks.push(String(chunk)); + if (typeof encoding === "function") encoding(); + else if (typeof callback === "function") callback(); + return true; + }; + try { + printHuman(result.report, result.bundleDir); + } finally { + process.stdout.write = originalWrite; + } + const actual = chunks.join("").replaceAll(result.bundleDir, ".out/runs/"); + assert.equal(actual, match[1]); +}); + test("pass bundle contains stage status, provenance, and only current artifacts", async () => { const outputRoot = tempRoot(); const result = await runDemo(["--response", "pass"], stubOptions(outputRoot, { runId: "pass-run" }));