oh-my-opencode 5.0.0-beta.33 → 5.0.0-beta.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +1 -3
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +1 -1
- package/.agents/skills/tech-debt-audit/SKILL.md +6 -76
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh-cn.md +1 -1
- package/THIRD-PARTY-NOTICES.md +27 -99
- package/dist/cli/index.js +48350 -48559
- package/dist/cli-node/index.js +48480 -48689
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/config/schema/oh-my-opencode-config.d.ts +0 -10
- package/dist/config/schema.d.ts +0 -1
- package/dist/create-hooks.d.ts +0 -1
- package/dist/hooks/index.d.ts +0 -1
- package/dist/index.js +11675 -13742
- package/dist/mcp/index.d.ts +0 -4
- package/dist/mcp/types.d.ts +0 -1
- package/dist/oh-my-opencode.schema.json +1 -45
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/shared/omo-process-sweep.d.ts +2 -4
- package/dist/skills/init-deep/SKILL.md +13 -13
- package/dist/skills/ulw-plan/SKILL.md +0 -1
- package/dist/tui.js +44809 -44910
- package/package.json +16 -18
- package/packages/omo-codex/THIRD-PARTY-NOTICES.md +0 -17
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -3
- package/packages/omo-codex/plugin/.mcp.json +0 -6
- package/packages/omo-codex/plugin/AGENTS.md +5 -5
- package/packages/omo-codex/plugin/README.md +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -15
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lcx/skills/lcx-doctor/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +0 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +12 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +45 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +35 -38
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +278 -193
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +7 -51
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +39 -22
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.d.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +64 -8
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +37 -18
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +81 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +32 -63
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +7 -53
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +50 -31
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +67 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +36 -17
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +26 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +47 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate-basics.test.ts +258 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-cap-and-dedupe.test.ts +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-poisoning-cascades.test.ts +97 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-single-report.test.ts +241 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -146
- package/packages/omo-codex/plugin/package.json +1 -2
- package/packages/omo-codex/plugin/scripts/AGENTS.md +0 -1
- package/packages/omo-codex/plugin/scripts/auto-update-plan.mjs +0 -1
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +0 -9
- package/packages/omo-codex/plugin/scripts/canonical-ultrawork-directive.mjs +17 -0
- package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +0 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -8
- package/packages/omo-codex/plugin/shared/src/config-loader.ts +2 -102
- package/packages/omo-codex/plugin/shared/src/config-migration.ts +0 -1
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +41 -130
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +13 -13
- package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +0 -1
- package/packages/omo-codex/plugin/test/AGENTS.md +2 -2
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +2 -21
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -2
- package/packages/omo-codex/plugin/test/aggregate-mcp.test.mjs +2 -7
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +0 -25
- package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +0 -31
- package/packages/omo-codex/plugin/test/canonical-ultrawork-directive.test.mjs +53 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +0 -50
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +0 -1
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -3
- package/packages/omo-codex/scripts/install-cache-copy.test.mjs +27 -1
- package/packages/omo-codex/scripts/install-config-git-bash.test.mjs +0 -23
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +238 -431
- package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +1 -25
- package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +0 -36
- package/packages/shared-skills/skills/init-deep/SKILL.md +13 -13
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +0 -1
- package/dist/config/schema/codegraph.d.ts +0 -12
- package/dist/hooks/codegraph-bootstrap/command-runner.d.ts +0 -17
- package/dist/hooks/codegraph-bootstrap/hook.d.ts +0 -35
- package/dist/hooks/codegraph-bootstrap/index.d.ts +0 -1
- package/dist/hooks/codegraph-bootstrap/project-root.d.ts +0 -1
- package/dist/hooks/codegraph-bootstrap/status.d.ts +0 -11
- package/dist/mcp/codegraph.d.ts +0 -16
- package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +0 -58
- package/packages/omo-codex/plugin/components/codegraph/LICENSE +0 -21
- package/packages/omo-codex/plugin/components/codegraph/NODE-RUNTIME-LICENSES.md +0 -2951
- package/packages/omo-codex/plugin/components/codegraph/NOTICE +0 -16
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +0 -12326
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +0 -9908
- package/packages/omo-codex/plugin/components/codegraph/package.json +0 -33
- package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +0 -29
- package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +0 -79
- package/packages/omo-codex/plugin/components/codegraph/src/hook-input.ts +0 -33
- package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +0 -25
- package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +0 -131
- package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +0 -258
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +0 -310
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +0 -92
- package/packages/omo-codex/plugin/components/codegraph/src/post-tool-use-hook.ts +0 -34
- package/packages/omo-codex/plugin/components/codegraph/src/serve-invocation.ts +0 -29
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +0 -263
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-command.ts +0 -106
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-cooldown.ts +0 -145
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-hook-runtime.ts +0 -22
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-lock.ts +0 -139
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-outcome.ts +0 -15
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-paths.ts +0 -32
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-project.ts +0 -109
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker-result.ts +0 -148
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +0 -175
- package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +0 -81
- package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +0 -34
- package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +0 -112
- package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +0 -26
- package/packages/omo-codex/plugin/components/codegraph/test/hook-session-start-guard.test.ts +0 -160
- package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +0 -50
- package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +0 -29
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +0 -320
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +0 -172
- package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +0 -91
- package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +0 -100
- package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +0 -76
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +0 -133
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +0 -245
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +0 -69
- package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +0 -43
- package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +0 -147
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +0 -47
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +0 -175
- package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +0 -385
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +0 -206
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-project.test.ts +0 -58
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-state.test.ts +0 -104
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-trust-boundary.test.ts +0 -65
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-availability.test.ts +0 -106
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-cooldown.test.ts +0 -116
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +0 -258
- package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +0 -56
- package/packages/omo-codex/plugin/components/codegraph/tsconfig.build.json +0 -13
- package/packages/omo-codex/plugin/components/codegraph/tsconfig.json +0 -25
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +0 -101
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +0 -18
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +0 -17
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot/editor.mjs +0 -125
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot/jsonc.mjs +0 -43
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot/scaffold.mjs +0 -29
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +0 -121
- package/packages/omo-codex/plugin/test/component-codegraph-mcp-smoke.test.mjs +0 -76
- package/packages/omo-codex/plugin/test/migrate-omo-sot.test.mjs +0 -179
|
@@ -73,6 +73,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
|
|
|
73
73
|
reportPath: artifactPath(base, "gate-review.md"),
|
|
74
74
|
evidence: "<replace:gate review evidence>",
|
|
75
75
|
blockers: [],
|
|
76
|
+
notes: [],
|
|
76
77
|
},
|
|
77
78
|
iteration: {
|
|
78
79
|
fullRerun: true,
|
|
@@ -115,13 +116,19 @@ export async function checkpointTemplate(
|
|
|
115
116
|
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
116
117
|
const hasAttempt = plan.evidenceLayoutVersion === 2 && active !== undefined;
|
|
117
118
|
const attemptDir = hasAttempt ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : ".omo/evidence";
|
|
119
|
+
const guidance = [
|
|
120
|
+
"codex-goal-json requires goal.objective to equal the plan's codexObjective verbatim; do not paraphrase it.",
|
|
121
|
+
"Fill every <replace:...> value with plausible non-empty evidence and use real, non-empty artifact files.",
|
|
122
|
+
'Passing codex-goal-json example: {"goal":{"objective":"<plan codexObjective verbatim>","status":"complete"}}.',
|
|
123
|
+
'Passing quality-gate-json example requires gateReview {"by":"category:deep","recommendation":"APPROVE","evidence":"review passed","reportPath":"<attemptDir>/gate-review.md","blockers":[],"notes":[]}, manualQa.artifactRefs objects, iteration, and criteriaCoverage.',
|
|
124
|
+
...(hasAttempt ? [] : ["This plan is evidence-layout v1; artifacts go under .omo/evidence/."]),
|
|
125
|
+
].join(" ");
|
|
118
126
|
return {
|
|
119
127
|
qualityGateTemplate: gateTemplate(resolveToolkitSurface(), attemptDir),
|
|
120
128
|
codexGoalTemplate: {
|
|
121
129
|
goal: { objective: plan.codexObjective ?? "<replace:codex objective>", status: "complete" },
|
|
122
130
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
: { guidance: "This plan is evidence-layout v1; artifacts go under .omo/evidence/." }),
|
|
131
|
+
guidance,
|
|
132
|
+
...(hasAttempt ? { attemptDir } : {}),
|
|
126
133
|
};
|
|
127
134
|
}
|
|
@@ -3,18 +3,11 @@ import { existsSync, statSync } from "node:fs";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./checkpoint-reconciliation.js";
|
|
10
|
-
import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
|
6
|
+
combineCheckpointValidationErrors,
|
|
7
|
+
validateCheckpointCodexGoal,
|
|
8
|
+
} from "./checkpoint-codex-validation.js";
|
|
11
9
|
import { requireAllCriteriaPass, requireAllPlanCriteriaPass, requireEssentialCriteriaPass } from "./evidence.js";
|
|
12
|
-
import {
|
|
13
|
-
codexGoalMode,
|
|
14
|
-
compatibleCodexObjectives,
|
|
15
|
-
expectedCodexObjective,
|
|
16
|
-
isFinalRunCompletionCandidate,
|
|
17
|
-
} from "./goal-status.js";
|
|
10
|
+
import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
18
11
|
import type { UlwLoopScope } from "./paths.js";
|
|
19
12
|
import { ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
20
13
|
import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
|
@@ -54,9 +47,6 @@ const QUALITY_GATE_FS = { existsSync, statSync } as const;
|
|
|
54
47
|
function ulwLoopFail(message: string, code: string): never {
|
|
55
48
|
throw new UlwLoopError(message, code);
|
|
56
49
|
}
|
|
57
|
-
function normalizeObjective(value: string): string {
|
|
58
|
-
return value.replace(/\s+/g, " ").trim();
|
|
59
|
-
}
|
|
60
50
|
function nonEmptyEvidence(value: string): string {
|
|
61
51
|
const trimmed = value.trim();
|
|
62
52
|
return trimmed || ulwLoopFail("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
|
|
@@ -178,62 +168,41 @@ export async function checkpointUlwLoop(
|
|
|
178
168
|
requireAllValidationBatchesClosed(plan, goal.id);
|
|
179
169
|
} else if (aggregate) requireEssentialCriteriaPass(goal);
|
|
180
170
|
else requireAllCriteriaPass(goal);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
objective !== undefined &&
|
|
195
|
-
normalizeObjective(objective) !== normalizeObjective(expectedCodexObjective(plan, goal));
|
|
196
|
-
const completedTaskScoped =
|
|
197
|
-
mismatchedTaskObjective &&
|
|
198
|
-
snapshot.status === "complete" &&
|
|
199
|
-
(await canReconcileCompletedTaskScopedAggregateSnapshot(
|
|
200
|
-
repoRoot,
|
|
201
|
-
plan,
|
|
202
|
-
goal,
|
|
203
|
-
objective,
|
|
204
|
-
evidence,
|
|
205
|
-
scope,
|
|
206
|
-
));
|
|
207
|
-
const activeFinalTaskScoped =
|
|
208
|
-
mismatchedTaskObjective &&
|
|
209
|
-
snapshot.status === "active" &&
|
|
210
|
-
(await canReconcileActiveFinalTaskScopedAggregateSnapshot(
|
|
211
|
-
repoRoot,
|
|
212
|
-
plan,
|
|
213
|
-
goal,
|
|
214
|
-
objective,
|
|
215
|
-
evidence,
|
|
216
|
-
scope,
|
|
217
|
-
));
|
|
218
|
-
const taskScoped = completedTaskScoped || activeFinalTaskScoped;
|
|
219
|
-
if (!taskScoped)
|
|
220
|
-
throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expectedCodexObjective(plan, goal), taskScopedHint: { goal, aggregate, final } });
|
|
171
|
+
let codexValidationError: UlwLoopError | undefined;
|
|
172
|
+
try {
|
|
173
|
+
codexGoal = await validateCheckpointCodexGoal({
|
|
174
|
+
repoRoot,
|
|
175
|
+
plan,
|
|
176
|
+
goal,
|
|
177
|
+
raw: args.codexGoalJson,
|
|
178
|
+
evidence,
|
|
179
|
+
...(scope === undefined ? {} : { scope }),
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!(error instanceof UlwLoopError)) throw error;
|
|
183
|
+
codexValidationError = error;
|
|
221
184
|
}
|
|
222
185
|
if (closesBatch) requireBatchFinalReady(plan, goal);
|
|
223
186
|
if (closesBatch && args.qualityGateJson === undefined)
|
|
224
187
|
throw new UlwLoopError("Validation batch final checkpoint requires --quality-gate-json.", "ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED");
|
|
225
188
|
if (final) aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
|
226
189
|
if (final || aggregateCompletion !== undefined || closesBatch) {
|
|
227
|
-
|
|
228
|
-
repoRoot,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
190
|
+
try {
|
|
191
|
+
qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
|
|
192
|
+
repoRoot,
|
|
193
|
+
fs: QUALITY_GATE_FS,
|
|
194
|
+
reviewerSurface: resolveToolkitSurface(),
|
|
195
|
+
...(plan.evidenceLayoutVersion === 2
|
|
196
|
+
? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
|
|
197
|
+
: {}),
|
|
198
|
+
});
|
|
199
|
+
requireBatchGate(plan, goal, qualityGate);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!(error instanceof UlwLoopError) || codexValidationError === undefined) throw error;
|
|
202
|
+
throw combineCheckpointValidationErrors(codexValidationError, error);
|
|
203
|
+
}
|
|
236
204
|
}
|
|
205
|
+
if (codexValidationError !== undefined) throw codexValidationError;
|
|
237
206
|
goal.status = "complete";
|
|
238
207
|
goal.completedAt = now;
|
|
239
208
|
goal.evidence = evidence;
|
|
@@ -1,57 +1,11 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import type { ValidateQualityGateOptions } from "./quality-gate.js";
|
|
3
|
-
import {
|
|
4
|
-
import { UlwLoopError } from "./types.js";
|
|
5
|
-
|
|
6
|
-
type Defect = { readonly field: string; readonly message: string };
|
|
7
|
-
const PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
8
|
-
|
|
9
|
-
function add(defects: Defect[], field: string, message: string): void {
|
|
10
|
-
defects.push({ field, message });
|
|
11
|
-
}
|
|
12
|
-
function text(value: unknown, field: string, defects: Defect[]): void {
|
|
13
|
-
if (typeof value !== "string" || value.trim() === "")
|
|
14
|
-
add(defects, field, `Final quality gate requires non-empty ${field}.`);
|
|
15
|
-
else if (PLACEHOLDER.test(value.trim())) add(defects, field, `Final quality gate rejects placeholder ${field}.`);
|
|
16
|
-
}
|
|
17
|
-
function section(value: unknown, field: string, defects: Defect[]): Record<string, unknown> {
|
|
18
|
-
if (!isRecord(value)) {
|
|
19
|
-
add(defects, field, `Final quality gate is missing ${field} evidence.`);
|
|
20
|
-
return {};
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
2
|
+
import { validateQualityGate } from "./quality-gate.js";
|
|
24
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Compatibility entry point for callers that used the former pre-pass API.
|
|
6
|
+
* Validation is performed by the real parser, which collects defects across
|
|
7
|
+
* every nested field before throwing one aggregate error.
|
|
8
|
+
*/
|
|
25
9
|
export function aggregateQualityGateDefects(input: unknown, opts: ValidateQualityGateOptions | undefined): void {
|
|
26
|
-
|
|
27
|
-
const defects: Defect[] = [];
|
|
28
|
-
const gate = input;
|
|
29
|
-
const manual = section(gate["manualQa"], "manualQa", defects);
|
|
30
|
-
const review = section(gate["gateReview"], "gateReview", defects);
|
|
31
|
-
text(manual["evidence"], "manualQa.evidence", defects);
|
|
32
|
-
text(review["evidence"], "gateReview.evidence", defects);
|
|
33
|
-
if (review["recommendation"] !== "APPROVE")
|
|
34
|
-
add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
|
|
35
|
-
const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
|
|
36
|
-
for (const [index, item] of artifacts.entries()) {
|
|
37
|
-
if (!isRecord(item)) continue;
|
|
38
|
-
const path = item["path"];
|
|
39
|
-
if (typeof path !== "string" || path.trim() === "") continue;
|
|
40
|
-
if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve(opts.repoRoot, path)))
|
|
41
|
-
add(
|
|
42
|
-
defects,
|
|
43
|
-
`manualQa.artifactRefs[${index}].path`,
|
|
44
|
-
`manualQa.artifactRefs[${index}].path must point to an existing artifact.`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
if (defects.length === 0) return;
|
|
48
|
-
const truncated = defects.length > 25;
|
|
49
|
-
const fields = defects.slice(0, 25).map(({ field, message }) => ({ field, message }));
|
|
50
|
-
const message = [
|
|
51
|
-
`Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
|
|
52
|
-
...fields.map((item) => `- ${item.field}: ${item.message}`),
|
|
53
|
-
].join("\n");
|
|
54
|
-
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
55
|
-
details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
|
|
56
|
-
});
|
|
10
|
+
validateQualityGate(input, opts);
|
|
57
11
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { isWithinAttemptDir } from "./paths.js";
|
|
3
3
|
import type { ValidateQualityGateOptions } from "./quality-gate.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
invalid,
|
|
6
|
+
isPoisoned,
|
|
7
|
+
markPoisonedArtifactKind,
|
|
8
|
+
section,
|
|
9
|
+
stringArray,
|
|
10
|
+
textField,
|
|
11
|
+
} from "./quality-gate-fields.js";
|
|
5
12
|
import type { UlwLoopManualQaArtifactKind, UlwLoopManualQaArtifactRef, UlwLoopManualQaSurface } from "./types.js";
|
|
6
13
|
|
|
7
14
|
export function surfaceField(value: unknown, field: string): UlwLoopManualQaSurface {
|
|
@@ -15,8 +22,8 @@ export function surfaceField(value: unknown, field: string): UlwLoopManualQaSurf
|
|
|
15
22
|
)
|
|
16
23
|
return value;
|
|
17
24
|
invalid(`${field} must be a supported manual QA surface.`, field);
|
|
25
|
+
return "cli";
|
|
18
26
|
}
|
|
19
|
-
|
|
20
27
|
export function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
|
|
21
28
|
if (
|
|
22
29
|
value === "cli-transcript" ||
|
|
@@ -28,8 +35,8 @@ export function kindField(value: unknown, field: string): UlwLoopManualQaArtifac
|
|
|
28
35
|
)
|
|
29
36
|
return value;
|
|
30
37
|
invalid(`${field} must be a supported artifact kind.`, field);
|
|
38
|
+
return "log";
|
|
31
39
|
}
|
|
32
|
-
|
|
33
40
|
export function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
|
|
34
41
|
switch (surface) {
|
|
35
42
|
case "cli":
|
|
@@ -44,24 +51,26 @@ export function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoo
|
|
|
44
51
|
return kind === "data-diff";
|
|
45
52
|
default:
|
|
46
53
|
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
54
|
+
return false;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
|
-
|
|
50
57
|
export function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
|
|
51
|
-
if (opts?.repoRoot === undefined || opts.fs === undefined) return;
|
|
58
|
+
if (opts?.repoRoot === undefined || opts.fs === undefined || isPoisoned(field)) return;
|
|
52
59
|
const absolute = resolve(opts.repoRoot, path);
|
|
53
|
-
if (!opts.fs.existsSync(absolute))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
57
|
-
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
58
|
-
invalid(
|
|
59
|
-
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
60
|
-
field,
|
|
61
|
-
);
|
|
60
|
+
if (!opts.fs.existsSync(absolute)) {
|
|
61
|
+
invalid(`${field} must point to an existing artifact.`, field);
|
|
62
|
+
return;
|
|
62
63
|
}
|
|
64
|
+
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
65
|
+
if (
|
|
66
|
+
opts.currentAttemptDir !== undefined &&
|
|
67
|
+
!isWithinAttemptDir(absolute, resolve(opts.repoRoot, opts.currentAttemptDir))
|
|
68
|
+
)
|
|
69
|
+
invalid(
|
|
70
|
+
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
71
|
+
field,
|
|
72
|
+
);
|
|
63
73
|
}
|
|
64
|
-
|
|
65
74
|
export function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
|
|
66
75
|
const byId = new Map<string, UlwLoopManualQaArtifactRef>();
|
|
67
76
|
for (const ref of refs) {
|
|
@@ -70,34 +79,44 @@ export function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<st
|
|
|
70
79
|
}
|
|
71
80
|
return byId;
|
|
72
81
|
}
|
|
73
|
-
|
|
74
82
|
export function parseArtifactRefs(
|
|
75
83
|
value: unknown,
|
|
76
84
|
opts?: ValidateQualityGateOptions,
|
|
77
85
|
): readonly UlwLoopManualQaArtifactRef[] {
|
|
78
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
86
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
79
87
|
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return value.flatMap((item, index) => {
|
|
91
|
+
const prefix = `manualQa.artifactRefs[${index}]`;
|
|
92
|
+
const ref = section(item, prefix);
|
|
93
|
+
if (isPoisoned(prefix)) return [];
|
|
94
|
+
const pathField = `${prefix}.path`;
|
|
95
|
+
const idField = `${prefix}.id`;
|
|
96
|
+
const kindFieldName = `${prefix}.kind`;
|
|
97
|
+
const descriptionField = `${prefix}.description`;
|
|
98
|
+
const path = textField(ref["path"], pathField);
|
|
99
|
+
const id = textField(ref["id"], idField);
|
|
100
|
+
const kind = kindField(ref["kind"], kindFieldName);
|
|
101
|
+
if (isPoisoned(kindFieldName)) markPoisonedArtifactKind(id);
|
|
102
|
+
const description = textField(ref["description"], descriptionField);
|
|
103
|
+
checkFile(path, pathField, opts);
|
|
104
|
+
return [{ id, kind, description, path }];
|
|
90
105
|
});
|
|
91
106
|
}
|
|
92
|
-
|
|
93
107
|
export function referencedArtifacts(
|
|
94
108
|
value: unknown,
|
|
95
109
|
field: string,
|
|
96
110
|
byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
|
|
97
111
|
): readonly UlwLoopManualQaArtifactRef[] {
|
|
98
|
-
|
|
112
|
+
const ids = stringArray(value, field);
|
|
113
|
+
if (isPoisoned(field)) return [];
|
|
114
|
+
return ids.flatMap((id) => {
|
|
99
115
|
const artifact = byId.get(id);
|
|
100
|
-
if (artifact === undefined)
|
|
101
|
-
|
|
116
|
+
if (artifact === undefined) {
|
|
117
|
+
invalid(`${field} references unknown artifact ${id}.`, field);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return [artifact];
|
|
102
121
|
});
|
|
103
122
|
}
|
|
@@ -1,44 +1,97 @@
|
|
|
1
1
|
import { UlwLoopError } from "./types.js";
|
|
2
2
|
|
|
3
3
|
const PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
4
|
+
type QualityGateDefect = { readonly field: string; readonly message: string };
|
|
5
|
+
type QualityGateCollector = {
|
|
6
|
+
readonly defects: QualityGateDefect[];
|
|
7
|
+
readonly poisonedFields: Set<string>;
|
|
8
|
+
readonly poisonedArtifactKinds: Set<string>;
|
|
9
|
+
};
|
|
10
|
+
let activeCollector: QualityGateCollector | undefined;
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
12
|
+
/** A recorded defect poisons only its field; consumers skip dependent checks while preserving valid siblings and entry identity. */
|
|
13
|
+
export function withQualityGateCollector<T>(operation: () => T): T {
|
|
14
|
+
const previous = activeCollector;
|
|
15
|
+
const collector: QualityGateCollector = { defects: [], poisonedFields: new Set(), poisonedArtifactKinds: new Set() };
|
|
16
|
+
activeCollector = collector;
|
|
17
|
+
try {
|
|
18
|
+
const result = operation();
|
|
19
|
+
if (collector.defects.length === 0) return result;
|
|
20
|
+
throwQualityGateDefects(collector.defects);
|
|
21
|
+
} finally {
|
|
22
|
+
activeCollector = previous;
|
|
23
|
+
}
|
|
7
24
|
}
|
|
8
25
|
|
|
26
|
+
export function throwQualityGateDefects(defects: readonly QualityGateDefect[]): never {
|
|
27
|
+
const uniqueDefects = [
|
|
28
|
+
...new Map(defects.map((defect) => [`${defect.field}\u0000${defect.message}`, defect])).values(),
|
|
29
|
+
];
|
|
30
|
+
const fields = uniqueDefects.slice(0, 25);
|
|
31
|
+
const truncated = uniqueDefects.length > fields.length;
|
|
32
|
+
const message = [
|
|
33
|
+
`Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
|
|
34
|
+
...fields.map((item) => `- ${item.field}: ${item.message}`),
|
|
35
|
+
].join("\n");
|
|
36
|
+
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
37
|
+
details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function invalid(message: string, field: string): undefined {
|
|
41
|
+
if (activeCollector === undefined)
|
|
42
|
+
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
|
43
|
+
activeCollector.defects.push({ field, message });
|
|
44
|
+
activeCollector.poisonedFields.add(field);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function isPoisoned(field: string): boolean {
|
|
48
|
+
return activeCollector?.poisonedFields.has(field) ?? false;
|
|
49
|
+
}
|
|
50
|
+
export function poisonField(field: string): void {
|
|
51
|
+
activeCollector?.poisonedFields.add(field);
|
|
52
|
+
}
|
|
53
|
+
export function markPoisonedArtifactKind(id: string): void {
|
|
54
|
+
activeCollector?.poisonedArtifactKinds.add(id);
|
|
55
|
+
}
|
|
56
|
+
export function isPoisonedArtifactKind(id: string): boolean {
|
|
57
|
+
return activeCollector?.poisonedArtifactKinds.has(id) ?? false;
|
|
58
|
+
}
|
|
9
59
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
60
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
61
|
}
|
|
12
|
-
|
|
13
62
|
export function section(value: unknown, field: string): Record<string, unknown> {
|
|
14
|
-
|
|
63
|
+
if (isRecord(value)) return value;
|
|
64
|
+
invalid(`Final quality gate is missing ${field} evidence.`, field);
|
|
65
|
+
return {};
|
|
15
66
|
}
|
|
16
|
-
|
|
17
67
|
export function textField(value: unknown, field: string): string {
|
|
18
|
-
if (typeof value !== "string" || value.trim() === "")
|
|
68
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
19
69
|
invalid(`Final quality gate requires non-empty ${field}.`, field);
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
20
72
|
const trimmed = value.trim();
|
|
21
73
|
if (PLACEHOLDER_PATTERN.test(trimmed)) invalid(`Final quality gate rejects placeholder ${field}.`, field);
|
|
22
74
|
return trimmed;
|
|
23
75
|
}
|
|
24
|
-
|
|
25
76
|
export function numberField(value: unknown, field: string): number {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
78
|
+
invalid(`Final quality gate requires numeric ${field}.`, field);
|
|
79
|
+
return 0;
|
|
29
80
|
}
|
|
30
|
-
|
|
31
81
|
export function stringArray(value: unknown, field: string): readonly string[] {
|
|
32
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
82
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
83
|
+
invalid(`Final quality gate requires ${field}.`, field);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
33
86
|
return value.map((item) => textField(item, field));
|
|
34
87
|
}
|
|
35
|
-
|
|
36
88
|
export function emptyBlockers(value: unknown, field: string): readonly [] {
|
|
37
89
|
if (Array.isArray(value) && value.length === 0) return [];
|
|
38
90
|
invalid(`${field} must be empty.`, field);
|
|
91
|
+
return [];
|
|
39
92
|
}
|
|
40
|
-
|
|
41
93
|
export function literal<T extends string | boolean>(value: unknown, expected: T, field: string): T {
|
|
42
94
|
if (value === expected) return expected;
|
|
43
95
|
invalid(`${field} must be ${String(expected)}.`, field);
|
|
96
|
+
return expected;
|
|
44
97
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { invalid, literal, textField } from "./quality-gate-fields.js";
|
|
2
2
|
|
|
3
3
|
export function passedVerdict(value: unknown, field: string): "passed" {
|
|
4
|
-
if (value === "not_applicable")
|
|
4
|
+
if (value === "not_applicable") {
|
|
5
|
+
invalid(`${field} must not be not_applicable.`, field);
|
|
6
|
+
return "passed";
|
|
7
|
+
}
|
|
5
8
|
return literal(value, "passed", field);
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export function codeQualityStatusField(value: unknown, field: string): "CLEAR" | "WATCH" {
|
|
9
12
|
if (value === "CLEAR" || value === "WATCH") return value;
|
|
10
13
|
invalid(`${field} must be CLEAR or WATCH.`, field);
|
|
14
|
+
return "CLEAR";
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export function adversarialVerdict(
|
|
@@ -20,4 +24,5 @@ export function adversarialVerdict(
|
|
|
20
24
|
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
21
25
|
}
|
|
22
26
|
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
27
|
+
return { verdict: "passed" };
|
|
23
28
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { aggregateQualityGateDefects } from "./quality-gate-aggregate.js";
|
|
2
1
|
import {
|
|
3
2
|
artifactCompatible,
|
|
4
3
|
artifactMap,
|
|
@@ -10,14 +9,17 @@ import {
|
|
|
10
9
|
import {
|
|
11
10
|
emptyBlockers,
|
|
12
11
|
invalid,
|
|
12
|
+
isPoisoned,
|
|
13
|
+
isPoisonedArtifactKind,
|
|
13
14
|
literal,
|
|
14
15
|
numberField,
|
|
15
16
|
section,
|
|
16
17
|
stringArray,
|
|
17
18
|
textField,
|
|
19
|
+
withQualityGateCollector,
|
|
18
20
|
} from "./quality-gate-fields.js";
|
|
19
21
|
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
20
|
-
import { GATE_SECTION_BY_ACCEPTOR,
|
|
22
|
+
import { GATE_SECTION_BY_ACCEPTOR, type UlwLoopToolkitSurface } from "./surface.js";
|
|
21
23
|
import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
|
|
22
24
|
|
|
23
25
|
export {
|
|
@@ -40,6 +42,10 @@ export interface ValidateQualityGateOptions {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function reviewerRoleField(value: unknown, expected: string, field: string): string {
|
|
45
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
46
|
+
textField(value, field);
|
|
47
|
+
return expected;
|
|
48
|
+
}
|
|
43
49
|
const actual = textField(value, field);
|
|
44
50
|
if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
|
|
45
51
|
return expected;
|
|
@@ -51,44 +57,51 @@ function reviewerAcceptorField(
|
|
|
51
57
|
sectionName: "manualQa" | "gateReview",
|
|
52
58
|
): string {
|
|
53
59
|
const field = `${sectionName}.by`;
|
|
54
|
-
const actual = textField(value, field);
|
|
55
60
|
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
61
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
62
|
+
textField(value, field);
|
|
63
|
+
return accepted?.[0] ?? "";
|
|
64
|
+
}
|
|
65
|
+
const actual = textField(value, field);
|
|
56
66
|
if (accepted === undefined || !accepted.includes(actual))
|
|
57
67
|
invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
|
|
58
68
|
return actual;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
72
|
+
return withQualityGateCollector(() => validateQualityGateUncollected(input, opts));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
62
76
|
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
63
|
-
aggregateQualityGateDefects(input, opts);
|
|
64
77
|
const gate = section(input, "qualityGate");
|
|
65
|
-
const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
|
|
66
|
-
for (const sectionName of requiredSections) section(gate[sectionName], sectionName);
|
|
67
78
|
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
68
79
|
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
69
80
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
70
81
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
71
82
|
const iteration = section(gate["iteration"], "iteration");
|
|
72
83
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
84
|
+
const codeReview = surface === "lazycodex" ? section(gate["codeReview"], "codeReview") : {};
|
|
73
85
|
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
74
86
|
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
const manualQaEvidence = textField(manualQa["evidence"], "manualQa.evidence");
|
|
88
|
+
const gateReviewEvidence = textField(gateReview["evidence"], "gateReview.evidence");
|
|
89
|
+
if (surface === "lazycodex") reviewerRoleField(codeReview?.["by"], "lazycodex-code-reviewer", "codeReview.by");
|
|
77
90
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
78
91
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
79
|
-
if (passCount < totalCriteria)
|
|
92
|
+
if (!isPoisoned("criteriaCoverage.passCount") && passCount < totalCriteria)
|
|
80
93
|
invalid("criteriaCoverage.passCount must cover totalCriteria.", "criteriaCoverage.passCount");
|
|
81
94
|
const artifactRefs = parseArtifactRefs(manualQa["artifactRefs"], opts);
|
|
82
95
|
const byId = artifactMap(artifactRefs);
|
|
83
96
|
const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
|
|
84
97
|
const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
|
|
85
98
|
const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
|
|
86
|
-
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
99
|
+
if (!isPoisoned("gateReview.reportPath")) checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
87
100
|
const common = {
|
|
88
101
|
manualQa: {
|
|
89
102
|
by: manualQaBy,
|
|
90
103
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
91
|
-
evidence:
|
|
104
|
+
evidence: manualQaEvidence,
|
|
92
105
|
surfaceEvidence,
|
|
93
106
|
adversarialCases,
|
|
94
107
|
artifactRefs,
|
|
@@ -97,7 +110,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
97
110
|
by: gateReviewBy,
|
|
98
111
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
99
112
|
reportPath: gateReportPath,
|
|
100
|
-
evidence:
|
|
113
|
+
evidence: gateReviewEvidence,
|
|
101
114
|
blockers: emptyBlockers(gateReview["blockers"], "gateReview.blockers"),
|
|
102
115
|
},
|
|
103
116
|
iteration: {
|
|
@@ -119,7 +132,6 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
119
132
|
},
|
|
120
133
|
};
|
|
121
134
|
if (surface === "omo-senpi") return { surface, ...common };
|
|
122
|
-
const codeReview = section(gate["codeReview"], "codeReview");
|
|
123
135
|
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
124
136
|
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
125
137
|
return {
|
|
@@ -140,10 +152,13 @@ function parseSurfaceEvidence(
|
|
|
140
152
|
value: unknown,
|
|
141
153
|
byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
|
|
142
154
|
): UlwLoopQualityGate["manualQa"]["surfaceEvidence"] {
|
|
143
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
155
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
144
156
|
invalid("manualQa.surfaceEvidence must not be empty.", "manualQa.surfaceEvidence");
|
|
145
|
-
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return value.flatMap((item, index) => {
|
|
146
160
|
const row = section(item, `manualQa.surfaceEvidence[${index}]`);
|
|
161
|
+
if (isPoisoned(`manualQa.surfaceEvidence[${index}]`)) return [];
|
|
147
162
|
const surface = surfaceField(row["surface"], `manualQa.surfaceEvidence[${index}].surface`);
|
|
148
163
|
const artifacts = referencedArtifacts(
|
|
149
164
|
row["artifactRefs"],
|
|
@@ -151,6 +166,7 @@ function parseSurfaceEvidence(
|
|
|
151
166
|
byId,
|
|
152
167
|
);
|
|
153
168
|
for (const artifact of artifacts) {
|
|
169
|
+
if (isPoisoned(`manualQa.surfaceEvidence[${index}].surface`) || isPoisonedArtifactKind(artifact.id)) continue;
|
|
154
170
|
if (!artifactCompatible(surface, artifact.kind)) {
|
|
155
171
|
invalid(
|
|
156
172
|
`manualQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`,
|
|
@@ -173,10 +189,13 @@ function parseAdversarialCases(
|
|
|
173
189
|
value: unknown,
|
|
174
190
|
byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
|
|
175
191
|
): UlwLoopQualityGate["manualQa"]["adversarialCases"] {
|
|
176
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
192
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
177
193
|
invalid("manualQa.adversarialCases must not be empty.", "manualQa.adversarialCases");
|
|
178
|
-
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
return value.flatMap((item, index) => {
|
|
179
197
|
const row = section(item, `manualQa.adversarialCases[${index}]`);
|
|
198
|
+
if (isPoisoned(`manualQa.adversarialCases[${index}]`)) return [];
|
|
180
199
|
const artifacts = referencedArtifacts(
|
|
181
200
|
row["artifactRefs"],
|
|
182
201
|
`manualQa.adversarialCases[${index}].artifactRefs`,
|