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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ project adheres to [Semantic Versioning](https://semver.org/).
- Playwright setup note in the Quick start section of `README.md`.

### Fixed
- Saved-case review now includes persisted stage stderr. `aas inspect` and
the Markdown review omitted the diagnostic that the run report already
stored, so a failed act looked like it had no child output.
- `runAct` now rejects a domain other than `refund` or `inventory` before it
spawns the rail CLI. Any other value was passed through as the demo
command.
Expand Down
6 changes: 3 additions & 3 deletions bin/case-review.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createHash } from "node:crypto";
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { assertRunsDirectory, DEFAULT_PATHS, UsageError, exportRunBundle, isValidRunId, summarizeRun } from "./aas.mjs";
import { assertRunsDirectory, clipChildStderr, DEFAULT_PATHS, UsageError, exportRunBundle, isValidRunId, summarizeRun } from "./aas.mjs";

export function listCasePage({ outputRoot = DEFAULT_PATHS.outputRoot, before = null, limit = 25, domain = null, outcome = null, search = null } = {}) {
if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw new Error("History limit must be an integer from 1 to 50.");
Expand Down Expand Up @@ -66,7 +66,7 @@ export function inspectCase(runId, options = {}) {
created_at: bundle.manifest.created_at ?? null, domain: report.domain ?? null,
requested_options: report.requested_options ?? null,
policy_failures: policyFailures,
stages: ['decide','act','prove'].map(name => ({ name, status: bundle.manifest.stages[name]?.status ?? 'unknown', reason: bundle.manifest.stages[name]?.reason ?? null, code: bundle.manifest.stages[name]?.code ?? null, artifact_available: Object.hasOwn(bundle.stages,name) })),
stages: ['decide','act','prove'].map(name => ({ name, status: bundle.manifest.stages[name]?.status ?? 'unknown', reason: bundle.manifest.stages[name]?.reason ?? null, code: bundle.manifest.stages[name]?.code ?? null, stderr: clipChildStderr(bundle.manifest.stages[name]?.stderr) || null, artifact_available: Object.hasOwn(bundle.stages,name) })),
policy_id: report.stages?.decide?.policy_id ?? null,
action_id: bundle.stages.act?.action_id ?? null, outcome: report.stages?.act?.outcome ?? null,
review_verdict: review?.verdict ?? null,
Expand Down Expand Up @@ -104,7 +104,7 @@ export function renderCaseMarkdown(review) {
for (const rule of review.policy_failures?.rules ?? []) lines.push('- '+markdownText(rule.rule_id)+': '+markdownText(rule.path)+'; '+markdownText(rule.kind)+'; '+markdownText(rule.reason_code));
if (review.policy_failures?.omitted) lines.push(review.policy_failures.omitted+' additional failures omitted; inspect the decide artifact for all records.');
lines.push('', ...stageHeading);
for(const stage of review.stages??[]) lines.push('- '+markdownText(stage.name)+': '+markdownText(stage.status)+'; artifact '+(stage.artifact_available?'present':'absent')+'; reason '+markdownText(stage.reason)+'; code '+markdownText(stage.code));
for(const stage of review.stages??[]) lines.push('- '+markdownText(stage.name)+': '+markdownText(stage.status)+'; artifact '+(stage.artifact_available?'present':'absent')+'; reason '+markdownText(stage.reason)+'; code '+markdownText(stage.code)+(stage.stderr?'; stderr '+markdownText(stage.stderr):''));
lines.push('', '## Evidence binding', '', '- Recorded digest: '+markdownText(review.recorded_evidence_digest), '- Recomputed digest: '+markdownText(review.recomputed_evidence_digest), '- Digests match: '+markdownText(review.digest_matches), '', '## Component revisions', '');
for(const entry of review.component_provenance??[]) lines.push('- '+markdownText(entry.name)+': '+markdownText(entry.commit));
lines.push('', '## Verification next step', '', '- Readiness: '+markdownText(review.verification_readiness?.state), '- Reason: '+markdownText(review.verification_readiness?.reason), '- Next step: '+markdownText(review.verification_readiness?.next_step));
Expand Down
23 changes: 23 additions & 0 deletions test/stack.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,29 @@ test('case review handoffs include requested settings and bounded policy failure
const markdown=renderCaseMarkdown(review);assert.match(markdown,/## Requested settings/);assert.match(markdown,/domain: inventory/);assert.match(markdown,/not\\_equal/);assert.match(markdown,/5 additional failures omitted/);assert.doesNotMatch(markdown,/OMIT_RAW/);
});

test("case review includes persisted stage stderr", async () => {
const { inspectCase, renderCaseMarkdown } = await import("../bin/case-review.mjs");
const outputRoot = tempRoot();
const runId = "stderr-review";
persistRunBundle({
outputRoot,
runId,
report: { run_id: runId, stages: {} },
stages: {
act: { status: "failed", stderr: "crctl: <boom>", raw: { outcome: null } },
},
componentProvenance: [],
exitCode: 1,
});
const review = inspectCase(runId, { outputRoot });
const act = review.stages.find((stage) => stage.name === "act");
assert.equal(act.stderr, "crctl: <boom>");
const markdown = renderCaseMarkdown(review);
assert.match(markdown, /stderr crctl: &lt;boom&gt;/);
assert.doesNotMatch(markdown, /<boom>/);
assert.equal(review.stages.find((stage) => stage.name === "decide").stderr, null);
});

test('case reviews explain verification readiness without claiming receipt verification', async () => {
const {inspectCase,renderCaseMarkdown}=await import('../bin/case-review.mjs');
const outputRoot=tempRoot(),rail={synthetic:true},digest='sha256:'+createHash('sha256').update(JSON.stringify(rail)).digest('hex');
Expand Down
Loading