@kontourai/flow-agents 3.2.0 → 3.4.0
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/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +173 -0
- package/CHANGELOG.md +54 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +310 -1
- package/build/src/cli/workflow-sidecar.js +1914 -126
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +30 -3
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +194 -22
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +5 -3
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +6 -2
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +23 -9
- package/docs/integrations/flow-agents-console.md +275 -0
- package/docs/integrations/index.md +4 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +78 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +389 -2
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +37 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +115 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +33 -2
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +113 -7
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +19 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +214 -253
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +194 -22
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/lib/reconcile-shape.js +381 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +1980 -119
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +29 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { parseArgs, flagList, flagString } from "../lib/args.js";
|
|
4
5
|
|
|
@@ -27,10 +28,46 @@ type FreshnessContext = {
|
|
|
27
28
|
scopeIntersections: string[];
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
type BoardIssue = {
|
|
32
|
+
id?: string;
|
|
33
|
+
node_id?: string;
|
|
34
|
+
owner?: string;
|
|
35
|
+
repo?: string;
|
|
36
|
+
number: number;
|
|
37
|
+
title: string;
|
|
38
|
+
body?: string;
|
|
39
|
+
state?: string;
|
|
40
|
+
labels: string[];
|
|
41
|
+
url?: string;
|
|
42
|
+
updatedAt?: string;
|
|
43
|
+
createdAt?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type BoardItem = {
|
|
47
|
+
id?: string;
|
|
48
|
+
position: number;
|
|
49
|
+
status?: string;
|
|
50
|
+
priority?: string;
|
|
51
|
+
issue: BoardIssue | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type RepoRef = {
|
|
55
|
+
owner: string;
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
30
59
|
function loadJson(file: string): unknown {
|
|
31
60
|
return file === "-" ? JSON.parse(fs.readFileSync(0, "utf8")) : JSON.parse(fs.readFileSync(file, "utf8"));
|
|
32
61
|
}
|
|
33
62
|
|
|
63
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
64
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function arrayValue(value: unknown): unknown[] {
|
|
68
|
+
return Array.isArray(value) ? value : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
34
71
|
function names(values: unknown): string[] {
|
|
35
72
|
return Array.isArray(values) ? values.flatMap((item) => typeof item === "object" && item !== null && typeof (item as Record<string, unknown>).name === "string" ? [(item as Record<string, string>).name] : []) : [];
|
|
36
73
|
}
|
|
@@ -38,7 +75,35 @@ function names(values: unknown): string[] {
|
|
|
38
75
|
function statusKey(value: unknown): string | null {
|
|
39
76
|
if (typeof value !== "string") return null;
|
|
40
77
|
const key = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
41
|
-
return ({ todo: "todo", to_do: "todo", ready: "ready", doing: "in_progress", in_progress: "in_progress", blocked: "blocked", review: "review", verification: "verification", done: "done", closed: "done" } as Record<string, string>)[key] ?? key;
|
|
78
|
+
return ({ open: "todo", todo: "todo", to_do: "todo", ready: "ready", doing: "in_progress", in_progress: "in_progress", blocked: "blocked", review: "review", verification: "verification", done: "done", closed: "done" } as Record<string, string>)[key] ?? key;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizedText(value: unknown): string | null {
|
|
82
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function repoKey(owner: unknown, repo: unknown, number: unknown): string | null {
|
|
86
|
+
const issueNumber = Number(number);
|
|
87
|
+
return typeof owner === "string" && typeof repo === "string" && Number.isFinite(issueNumber)
|
|
88
|
+
? `${owner}/${repo}#${issueNumber}`
|
|
89
|
+
: null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function configuredRepos(settings: Record<string, unknown>): RepoRef[] {
|
|
93
|
+
const workProvider = settings.work_item_provider as Record<string, unknown>;
|
|
94
|
+
const boardProvider = (settings.board_provider ?? {}) as Record<string, unknown>;
|
|
95
|
+
const workRepo = workProvider.repo as Record<string, string>;
|
|
96
|
+
const board = (boardProvider.board ?? {}) as Record<string, unknown>;
|
|
97
|
+
const defaultOwner = stringValue(board.owner) ?? stringValue((boardProvider.repo as Record<string, unknown> | undefined)?.owner) ?? workRepo.owner;
|
|
98
|
+
const workspace = asRecord(settings.workspace);
|
|
99
|
+
const repos = arrayValue(workspace?.repos).flatMap((repo) => {
|
|
100
|
+
if (typeof repo === "string" && repo.trim()) return [{ owner: defaultOwner, name: repo.trim() }];
|
|
101
|
+
const record = asRecord(repo);
|
|
102
|
+
const owner = stringValue(record?.owner) ?? defaultOwner;
|
|
103
|
+
const name = stringValue(record?.name);
|
|
104
|
+
return owner && name ? [{ owner, name }] : [];
|
|
105
|
+
});
|
|
106
|
+
return repos.length ? repos : [{ owner: workRepo.owner, name: workRepo.name }];
|
|
42
107
|
}
|
|
43
108
|
|
|
44
109
|
function projectStatus(issue: Record<string, unknown>): string | null {
|
|
@@ -412,6 +477,305 @@ function normalize(issue: Record<string, unknown>, settings: Record<string, unkn
|
|
|
412
477
|
};
|
|
413
478
|
}
|
|
414
479
|
|
|
480
|
+
function fieldValueByName(item: Record<string, unknown>, fieldName: string): string | undefined {
|
|
481
|
+
const wanted = fieldName.toLowerCase();
|
|
482
|
+
const direct = item[fieldName] ?? item[fieldName.toLowerCase()];
|
|
483
|
+
if (typeof direct === "string") return direct;
|
|
484
|
+
const fieldValues = Array.isArray(item.fieldValues) ? item.fieldValues : arrayValue(asRecord(item.fieldValues)?.nodes);
|
|
485
|
+
for (const value of fieldValues) {
|
|
486
|
+
const record = asRecord(value);
|
|
487
|
+
if (!record) continue;
|
|
488
|
+
const field = asRecord(record.field);
|
|
489
|
+
const name = normalizedText(field?.name);
|
|
490
|
+
if (name !== wanted) continue;
|
|
491
|
+
return stringValue(record.name) ?? stringValue(record.text) ?? stringValue(record.value) ?? stringValue(record.optionName);
|
|
492
|
+
}
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeBoardIssue(rawIssue: unknown, fallbackOwner?: string, fallbackRepo?: string): BoardIssue | null {
|
|
497
|
+
const issue = asRecord(rawIssue);
|
|
498
|
+
if (!issue) return null;
|
|
499
|
+
const number = Number(issue.number);
|
|
500
|
+
if (!Number.isFinite(number)) return null;
|
|
501
|
+
const repository = asRecord(issue.repository);
|
|
502
|
+
const owner = stringValue(issue.owner) ?? stringValue(asRecord(repository?.owner)?.login) ?? fallbackOwner;
|
|
503
|
+
const repo = stringValue(issue.repo) ?? stringValue(issue.repositoryName) ?? stringValue(repository?.name) ?? fallbackRepo;
|
|
504
|
+
return {
|
|
505
|
+
id: stringValue(issue.id),
|
|
506
|
+
node_id: stringValue(issue.node_id) ?? stringValue(issue.id),
|
|
507
|
+
owner,
|
|
508
|
+
repo,
|
|
509
|
+
number,
|
|
510
|
+
title: stringValue(issue.title) ?? `#${number}`,
|
|
511
|
+
body: stringValue(issue.body) ?? "",
|
|
512
|
+
state: stringValue(issue.state) ?? "OPEN",
|
|
513
|
+
labels: names(issue.labels).concat(arrayValue(asRecord(issue.labels)?.nodes).flatMap((label) => stringValue(asRecord(label)?.name) ?? [])),
|
|
514
|
+
url: stringValue(issue.url),
|
|
515
|
+
updatedAt: stringValue(issue.updatedAt),
|
|
516
|
+
createdAt: stringValue(issue.createdAt),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function normalizeBoardItem(rawItem: unknown, index: number, fallbackOwner?: string, fallbackRepo?: string): BoardItem | null {
|
|
521
|
+
const item = asRecord(rawItem);
|
|
522
|
+
if (!item) return null;
|
|
523
|
+
const content = item.content ?? item.issue;
|
|
524
|
+
const issue = normalizeBoardIssue(content, fallbackOwner, fallbackRepo);
|
|
525
|
+
if (!issue) return null;
|
|
526
|
+
const rawPosition = item.position ?? item.databaseId ?? item.index;
|
|
527
|
+
const position = Number.isFinite(Number(rawPosition)) ? Number(rawPosition) : index;
|
|
528
|
+
return {
|
|
529
|
+
id: stringValue(item.id),
|
|
530
|
+
position,
|
|
531
|
+
status: fieldValueByName(item, "Status") ?? stringValue(item.status),
|
|
532
|
+
priority: fieldValueByName(item, "Priority") ?? stringValue(item.priority),
|
|
533
|
+
issue,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function boardItemsFromDoc(doc: unknown, settings: Record<string, unknown>): BoardItem[] {
|
|
538
|
+
const workProvider = settings.work_item_provider as Record<string, unknown>;
|
|
539
|
+
const repo = workProvider.repo as Record<string, string>;
|
|
540
|
+
const record = asRecord(doc);
|
|
541
|
+
const rawItems = Array.isArray(doc)
|
|
542
|
+
? doc
|
|
543
|
+
: arrayValue(record?.project_items).length ? arrayValue(record?.project_items)
|
|
544
|
+
: arrayValue(record?.projectItems).length ? arrayValue(record?.projectItems)
|
|
545
|
+
: arrayValue(record?.items).length ? arrayValue(record?.items)
|
|
546
|
+
: arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items).length
|
|
547
|
+
? arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items)
|
|
548
|
+
: [];
|
|
549
|
+
const nodes = rawItems.flatMap((item) => asRecord(item)?.nodes && Array.isArray(asRecord(item)?.nodes) ? arrayValue(asRecord(item)?.nodes) : [item]);
|
|
550
|
+
return nodes.flatMap((item, index) => normalizeBoardItem(item, index, repo.owner, repo.name) ?? []);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function openIssuesFromDoc(doc: unknown, settings: Record<string, unknown>): BoardIssue[] {
|
|
554
|
+
const repos = configuredRepos(settings);
|
|
555
|
+
const fallback = repos.length === 1 ? repos[0] : undefined;
|
|
556
|
+
const record = asRecord(doc);
|
|
557
|
+
const rawOpenIssues = record?.open_issues;
|
|
558
|
+
const rawIssues = arrayValue(rawOpenIssues).length ? arrayValue(rawOpenIssues)
|
|
559
|
+
: rawOpenIssues && typeof rawOpenIssues === "object" && !Array.isArray(rawOpenIssues)
|
|
560
|
+
? Object.entries(rawOpenIssues as Record<string, unknown>).flatMap(([repoName, issues]) => arrayValue(issues).map((issue) => ({ issue, repoName })))
|
|
561
|
+
: arrayValue(record?.issues).length ? arrayValue(record?.issues)
|
|
562
|
+
: arrayValue(asRecord(asRecord(asRecord(record?.data)?.repository)?.issues)?.nodes);
|
|
563
|
+
return rawIssues.flatMap((issue) => {
|
|
564
|
+
const wrapped = asRecord(issue);
|
|
565
|
+
const rawIssue = wrapped && "issue" in wrapped ? wrapped.issue : issue;
|
|
566
|
+
const repoName = wrapped && typeof wrapped.repoName === "string" ? wrapped.repoName : undefined;
|
|
567
|
+
const repoFallback = repoName ? repos.find((repo) => repo.name === repoName) : fallback;
|
|
568
|
+
const normalized = normalizeBoardIssue(rawIssue, repoFallback?.owner, repoName ?? repoFallback?.name);
|
|
569
|
+
return normalized && (normalized.state ?? "OPEN").toUpperCase() === "OPEN" ? [normalized] : [];
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function boardCandidate(item: BoardItem, settings: Record<string, unknown>): Record<string, unknown> {
|
|
574
|
+
const workProvider = settings.work_item_provider as Record<string, unknown>;
|
|
575
|
+
const boardProvider = (settings.board_provider ?? {}) as Record<string, unknown>;
|
|
576
|
+
const issue = item.issue as BoardIssue;
|
|
577
|
+
const owner = issue.owner ?? ((workProvider.repo as Record<string, string>).owner);
|
|
578
|
+
const repo = issue.repo ?? ((workProvider.repo as Record<string, string>).name);
|
|
579
|
+
const body = issue.body ?? "";
|
|
580
|
+
const metadata = parseWorkItemMetadata(body);
|
|
581
|
+
const rawStatus = item.status ?? "Todo";
|
|
582
|
+
return {
|
|
583
|
+
id: `github:${owner}/${repo}#${issue.number}`,
|
|
584
|
+
title: issue.title,
|
|
585
|
+
body,
|
|
586
|
+
status: statusKey(rawStatus) ?? rawStatus.toLowerCase(),
|
|
587
|
+
labels: issue.labels,
|
|
588
|
+
priority: item.priority,
|
|
589
|
+
...sourceRevisionFields(metadata, owner, repo),
|
|
590
|
+
blockers: mergeBlockers(normalizeStructuredBlockers(metadata, owner, repo), blockers(body, owner, repo)),
|
|
591
|
+
related_links: extractRefs(body, owner, repo),
|
|
592
|
+
source_provider: { role: "WorkItemProvider", kind: workProvider.kind, owner, repo, number: issue.number, url: issue.url, state: issue.state, node_id: issue.node_id ?? issue.id, capabilities: workProvider.capabilities ?? [] },
|
|
593
|
+
board_membership: {
|
|
594
|
+
role: "BoardProvider",
|
|
595
|
+
kind: boardProvider.kind,
|
|
596
|
+
board: boardProvider.board,
|
|
597
|
+
project_item_id: item.id,
|
|
598
|
+
position: item.position,
|
|
599
|
+
status: rawStatus,
|
|
600
|
+
priority: item.priority,
|
|
601
|
+
capabilities: boardProvider.capabilities ?? [],
|
|
602
|
+
},
|
|
603
|
+
artifact_refs: Array.from(body.matchAll(FLOW_ARTIFACT_PATTERN), (m) => (m.groups?.path ?? "").replace(/[.,]+$/, "")),
|
|
604
|
+
updated_at: issue.updatedAt,
|
|
605
|
+
created_at: issue.createdAt,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function priorityRank(priority: unknown): number {
|
|
610
|
+
const key = String(priority ?? "").trim().toUpperCase();
|
|
611
|
+
return ({ P0: 0, P1: 1, P2: 2 } as Record<string, number>)[key] ?? 99;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function boardResult(settings: Record<string, unknown>, boardItems: BoardItem[], openIssues: BoardIssue[]): Record<string, unknown> {
|
|
615
|
+
const selection = (settings.selection ?? {}) as Record<string, unknown>;
|
|
616
|
+
const filters = (selection.filters ?? {}) as Record<string, unknown>;
|
|
617
|
+
const readyStatuses = new Set(((filters.ready_statuses as string[] | undefined) ?? ["ready"]).map((status) => status.toLowerCase()));
|
|
618
|
+
const workProvider = settings.work_item_provider as Record<string, unknown>;
|
|
619
|
+
const repo = workProvider.repo as Record<string, string>;
|
|
620
|
+
const candidates = boardItems.map((item) => ({ item, candidate: boardCandidate(item, settings) }));
|
|
621
|
+
const readyQueue = candidates
|
|
622
|
+
.filter(({ item }) => readyStatuses.has(String(statusKey(item.status) ?? item.status ?? "").toLowerCase()))
|
|
623
|
+
.sort((left, right) => priorityRank(left.item.priority) - priorityRank(right.item.priority) || left.item.position - right.item.position)
|
|
624
|
+
.map(({ candidate }) => candidate);
|
|
625
|
+
const boardIssueKeys = new Set(boardItems.flatMap((item) => {
|
|
626
|
+
const issue = item.issue;
|
|
627
|
+
const key = repoKey(issue?.owner ?? repo.owner, issue?.repo ?? repo.name, issue?.number);
|
|
628
|
+
return key ? [key] : [];
|
|
629
|
+
}));
|
|
630
|
+
const intakeGaps = openIssues
|
|
631
|
+
.filter((issue) => !boardIssueKeys.has(repoKey(issue.owner ?? repo.owner, issue.repo ?? repo.name, issue.number) ?? ""))
|
|
632
|
+
.map((issue) => ({
|
|
633
|
+
id: `github:${issue.owner ?? repo.owner}/${issue.repo ?? repo.name}#${issue.number}`,
|
|
634
|
+
title: issue.title,
|
|
635
|
+
status: statusKey(issue.state) ?? "todo",
|
|
636
|
+
labels: issue.labels,
|
|
637
|
+
source_provider: { role: "WorkItemProvider", kind: workProvider.kind, owner: issue.owner ?? repo.owner, repo: issue.repo ?? repo.name, number: issue.number, url: issue.url, state: issue.state, node_id: issue.node_id ?? issue.id, capabilities: workProvider.capabilities ?? [] },
|
|
638
|
+
}));
|
|
639
|
+
const warnings = readyQueue.length === 0
|
|
640
|
+
? [{ code: "zero_ready_items", message: "Configured BoardProvider yielded zero ready items; treat this as a dead readiness source and do not silently fall back to WorkItemProvider issue listing." }]
|
|
641
|
+
: [];
|
|
642
|
+
return { ready_queue: readyQueue, intake_gaps: intakeGaps, warnings };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const PROJECT_BOARD_QUERY = `
|
|
646
|
+
query($org: String!, $projectNumber: Int!, $repoOwner: String!, $repoName: String!, $itemCursor: String, $issueCursor: String) {
|
|
647
|
+
organization(login: $org) {
|
|
648
|
+
projectV2(number: $projectNumber) {
|
|
649
|
+
items(first: 100, after: $itemCursor) {
|
|
650
|
+
pageInfo { hasNextPage endCursor }
|
|
651
|
+
nodes {
|
|
652
|
+
id
|
|
653
|
+
fieldValues(first: 30) {
|
|
654
|
+
nodes {
|
|
655
|
+
... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2FieldCommon { name } } }
|
|
656
|
+
... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2FieldCommon { name } } }
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
content {
|
|
660
|
+
... on Issue {
|
|
661
|
+
id
|
|
662
|
+
number
|
|
663
|
+
title
|
|
664
|
+
body
|
|
665
|
+
state
|
|
666
|
+
url
|
|
667
|
+
updatedAt
|
|
668
|
+
createdAt
|
|
669
|
+
repository { name owner { login } }
|
|
670
|
+
labels(first: 50) { nodes { name } }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
repository(owner: $repoOwner, name: $repoName) {
|
|
678
|
+
issues(first: 100, after: $issueCursor, states: OPEN) {
|
|
679
|
+
pageInfo { hasNextPage endCursor }
|
|
680
|
+
nodes {
|
|
681
|
+
id
|
|
682
|
+
number
|
|
683
|
+
title
|
|
684
|
+
body
|
|
685
|
+
state
|
|
686
|
+
url
|
|
687
|
+
updatedAt
|
|
688
|
+
createdAt
|
|
689
|
+
repository { name owner { login } }
|
|
690
|
+
labels(first: 50) { nodes { name } }
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}`;
|
|
695
|
+
|
|
696
|
+
const REPO_ISSUES_QUERY = `
|
|
697
|
+
query($repoOwner: String!, $repoName: String!, $issueCursor: String) {
|
|
698
|
+
repository(owner: $repoOwner, name: $repoName) {
|
|
699
|
+
issues(first: 100, after: $issueCursor, states: OPEN) {
|
|
700
|
+
pageInfo { hasNextPage endCursor }
|
|
701
|
+
nodes {
|
|
702
|
+
id
|
|
703
|
+
number
|
|
704
|
+
title
|
|
705
|
+
body
|
|
706
|
+
state
|
|
707
|
+
url
|
|
708
|
+
updatedAt
|
|
709
|
+
createdAt
|
|
710
|
+
repository { name owner { login } }
|
|
711
|
+
labels(first: 50) { nodes { name } }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}`;
|
|
716
|
+
|
|
717
|
+
function ghGraphql(query: string, variables: Record<string, string | number | undefined>): Record<string, unknown> {
|
|
718
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
719
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
720
|
+
if (value !== undefined && value !== "") args.push("-F", `${key}=${value}`);
|
|
721
|
+
}
|
|
722
|
+
return JSON.parse(execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })) as Record<string, unknown>;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function liveBoardDoc(settings: Record<string, unknown>): Record<string, unknown> {
|
|
726
|
+
const workProvider = settings.work_item_provider as Record<string, unknown>;
|
|
727
|
+
const boardProvider = settings.board_provider as Record<string, unknown>;
|
|
728
|
+
const workRepo = workProvider.repo as Record<string, string>;
|
|
729
|
+
const repos = configuredRepos(settings);
|
|
730
|
+
const board = boardProvider.board as Record<string, unknown>;
|
|
731
|
+
const org = stringValue(board.owner) ?? stringValue((boardProvider.repo as Record<string, unknown> | undefined)?.owner) ?? workRepo.owner;
|
|
732
|
+
const projectNumber = Number(board.number);
|
|
733
|
+
if (!org || !Number.isFinite(projectNumber)) throw new Error("BoardProvider must configure board.owner or repo.owner and board.number for live reads");
|
|
734
|
+
const items: unknown[] = [];
|
|
735
|
+
const issues: unknown[] = [];
|
|
736
|
+
let itemCursor = "";
|
|
737
|
+
let issueCursor = "";
|
|
738
|
+
let fetchItems = true;
|
|
739
|
+
let fetchIssues = true;
|
|
740
|
+
while (fetchItems || fetchIssues) {
|
|
741
|
+
const page = ghGraphql(PROJECT_BOARD_QUERY, {
|
|
742
|
+
org,
|
|
743
|
+
projectNumber,
|
|
744
|
+
repoOwner: repos[0]?.owner ?? workRepo.owner,
|
|
745
|
+
repoName: repos[0]?.name ?? workRepo.name,
|
|
746
|
+
itemCursor,
|
|
747
|
+
issueCursor,
|
|
748
|
+
});
|
|
749
|
+
const projectItems = asRecord(asRecord(asRecord(page.data)?.organization)?.projectV2)?.items as Record<string, unknown> | undefined;
|
|
750
|
+
const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues as Record<string, unknown> | undefined;
|
|
751
|
+
if (fetchItems) items.push(...arrayValue(projectItems?.nodes));
|
|
752
|
+
if (fetchIssues) issues.push(...arrayValue(repoIssues?.nodes));
|
|
753
|
+
const itemPageInfo = asRecord(projectItems?.pageInfo);
|
|
754
|
+
const issuePageInfo = asRecord(repoIssues?.pageInfo);
|
|
755
|
+
fetchItems = Boolean(itemPageInfo?.hasNextPage);
|
|
756
|
+
fetchIssues = Boolean(issuePageInfo?.hasNextPage);
|
|
757
|
+
itemCursor = stringValue(itemPageInfo?.endCursor) ?? "";
|
|
758
|
+
issueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
|
|
759
|
+
}
|
|
760
|
+
for (const repo of repos.slice(1)) {
|
|
761
|
+
let repoIssueCursor = "";
|
|
762
|
+
let fetchRepoIssues = true;
|
|
763
|
+
while (fetchRepoIssues) {
|
|
764
|
+
const page = ghGraphql(REPO_ISSUES_QUERY, {
|
|
765
|
+
repoOwner: repo.owner,
|
|
766
|
+
repoName: repo.name,
|
|
767
|
+
issueCursor: repoIssueCursor,
|
|
768
|
+
});
|
|
769
|
+
const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues as Record<string, unknown> | undefined;
|
|
770
|
+
issues.push(...arrayValue(repoIssues?.nodes));
|
|
771
|
+
const issuePageInfo = asRecord(repoIssues?.pageInfo);
|
|
772
|
+
fetchRepoIssues = Boolean(issuePageInfo?.hasNextPage);
|
|
773
|
+
repoIssueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { items, issues };
|
|
777
|
+
}
|
|
778
|
+
|
|
415
779
|
function classify(item: Record<string, unknown>, settings: Record<string, unknown>, resolved: Record<string, string>): Record<string, unknown> {
|
|
416
780
|
const selection = (settings.selection ?? {}) as Record<string, unknown>;
|
|
417
781
|
const filters = (selection.filters ?? {}) as Record<string, unknown>;
|
|
@@ -448,7 +812,11 @@ function resolveSettings(doc: Record<string, unknown>): Record<string, unknown>
|
|
|
448
812
|
if (typeof doc.settings === "object" && doc.settings !== null) return doc.settings as Record<string, unknown>;
|
|
449
813
|
if (doc.work_item_provider) return doc;
|
|
450
814
|
if (Array.isArray(doc.projects) && doc.projects.length) return doc.projects[0] as Record<string, unknown>;
|
|
451
|
-
if (typeof doc.defaults === "object" && doc.defaults !== null)
|
|
815
|
+
if (typeof doc.defaults === "object" && doc.defaults !== null) {
|
|
816
|
+
const settings = structuredClone(doc.defaults) as Record<string, unknown>;
|
|
817
|
+
if (typeof doc.workspace === "object" && doc.workspace !== null) settings.workspace = doc.workspace;
|
|
818
|
+
return settings;
|
|
819
|
+
}
|
|
452
820
|
throw new Error("settings JSON must be an effective settings object or backlog-provider-settings document");
|
|
453
821
|
}
|
|
454
822
|
|
|
@@ -456,7 +824,8 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
456
824
|
const args = parseArgs(argv);
|
|
457
825
|
const settingsJson = flagString(args.flags, "settings-json");
|
|
458
826
|
const issuesJson = flagString(args.flags, "issues-json");
|
|
459
|
-
|
|
827
|
+
const itemsJson = flagString(args.flags, "items-json");
|
|
828
|
+
if (!settingsJson) throw new Error("--settings-json is required");
|
|
460
829
|
const now = new Date(flagString(args.flags, "now") ?? Date.now());
|
|
461
830
|
if (Number.isNaN(now.getTime())) throw new Error("--now must be a valid date/time when provided");
|
|
462
831
|
const freshnessInputs: FreshnessInputs = {
|
|
@@ -467,6 +836,11 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
467
836
|
now,
|
|
468
837
|
};
|
|
469
838
|
const settings = resolveSettings(loadJson(settingsJson) as Record<string, unknown>);
|
|
839
|
+
if (itemsJson || !issuesJson) {
|
|
840
|
+
const doc = itemsJson ? loadJson(itemsJson) : liveBoardDoc(settings);
|
|
841
|
+
console.log(JSON.stringify(boardResult(settings, boardItemsFromDoc(doc, settings), openIssuesFromDoc(doc, settings)), null, 2));
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
470
844
|
const issuesDoc = loadJson(issuesJson);
|
|
471
845
|
const issues = Array.isArray(issuesDoc) ? issuesDoc : ((issuesDoc as Record<string, unknown>).items as unknown[] ?? [issuesDoc]);
|
|
472
846
|
const resolved = Object.fromEntries(flagList(args.flags, "resolved-ref").map((item) => item.split("=", 2) as [string, string]));
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
validateEvidenceRef,
|
|
18
18
|
normalizeEvidenceRefs,
|
|
19
19
|
normalizeCheck,
|
|
20
|
+
reduceCaptureLogByCommand,
|
|
20
21
|
} from "../../build/src/cli/workflow-sidecar.js";
|
|
21
22
|
import { buildClaimExplanation } from "../../build/src/cli/sidecar-claim-explain.js";
|
|
22
23
|
|
|
@@ -111,6 +112,69 @@ test("normalizeCheck: validates required fields, kind, and status", () => {
|
|
|
111
112
|
assert.equal(ok.status, "pass");
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
// ── reduceCaptureLogByCommand (#470 iteration 2, finding #2 — three-way capture fold) ────
|
|
116
|
+
|
|
117
|
+
test("reduceCaptureLogByCommand: a single ambiguous entry classifies as ambiguous (non-confirming), not pass", () => {
|
|
118
|
+
const out = reduceCaptureLogByCommand([{ command: "npm test", observedResult: "ambiguous", exitCode: null }]);
|
|
119
|
+
const entry = out.get("npm test");
|
|
120
|
+
assert.equal(entry.observedResult, "ambiguous");
|
|
121
|
+
assert.equal(entry.exitCode, null);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("reduceCaptureLogByCommand: fail beats pass and ambiguous for the same command", () => {
|
|
125
|
+
const out = reduceCaptureLogByCommand([
|
|
126
|
+
{ command: "npm test", observedResult: "pass", exitCode: 0 },
|
|
127
|
+
{ command: "npm test", observedResult: "ambiguous", exitCode: null },
|
|
128
|
+
{ command: "npm test", observedResult: "fail", exitCode: 1 },
|
|
129
|
+
]);
|
|
130
|
+
const entry = out.get("npm test");
|
|
131
|
+
assert.equal(entry.observedResult, "fail");
|
|
132
|
+
assert.equal(entry.exitCode, 1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("reduceCaptureLogByCommand: fail beats pass and ambiguous regardless of entry order", () => {
|
|
136
|
+
const out = reduceCaptureLogByCommand([
|
|
137
|
+
{ command: "npm test", observedResult: "fail", exitCode: 1 },
|
|
138
|
+
{ command: "npm test", observedResult: "pass", exitCode: 0 },
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(out.get("npm test").observedResult, "fail");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("reduceCaptureLogByCommand: pass beats ambiguous when there is no fail", () => {
|
|
144
|
+
const out = reduceCaptureLogByCommand([
|
|
145
|
+
{ command: "npm test", observedResult: "ambiguous", exitCode: null },
|
|
146
|
+
{ command: "npm test", observedResult: "pass", exitCode: 0 },
|
|
147
|
+
]);
|
|
148
|
+
const entry = out.get("npm test");
|
|
149
|
+
assert.equal(entry.observedResult, "pass");
|
|
150
|
+
assert.equal(entry.exitCode, 0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("reduceCaptureLogByCommand: legacy entries (no observedResult) classify from exitCode alone", () => {
|
|
154
|
+
const out = reduceCaptureLogByCommand([
|
|
155
|
+
{ command: "legacy-pass", exitCode: 0 },
|
|
156
|
+
{ command: "legacy-fail", exitCode: 1 },
|
|
157
|
+
{ command: "legacy-ambiguous", exitCode: null },
|
|
158
|
+
]);
|
|
159
|
+
assert.equal(out.get("legacy-pass").observedResult, "pass");
|
|
160
|
+
assert.equal(out.get("legacy-fail").observedResult, "fail");
|
|
161
|
+
assert.equal(out.get("legacy-ambiguous").observedResult, "ambiguous");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("reduceCaptureLogByCommand: legacy nonzero exit code classifies as fail (never coerced to pass)", () => {
|
|
165
|
+
const out = reduceCaptureLogByCommand([{ command: "some cmd", exitCode: 2 }]);
|
|
166
|
+
const entry = out.get("some cmd");
|
|
167
|
+
assert.equal(entry.observedResult, "fail");
|
|
168
|
+
assert.equal(entry.exitCode, 2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("reduceCaptureLogByCommand: grep/diff absence-ambiguous entry (observedResult:'ambiguous', exitCode:1) stays ambiguous, not fail", () => {
|
|
172
|
+
const out = reduceCaptureLogByCommand([{ command: "grep -q needle file", observedResult: "ambiguous", exitCode: 1 }]);
|
|
173
|
+
const entry = out.get("grep -q needle file");
|
|
174
|
+
assert.equal(entry.observedResult, "ambiguous");
|
|
175
|
+
assert.equal(entry.exitCode, 1);
|
|
176
|
+
});
|
|
177
|
+
|
|
114
178
|
// ── buildGateInquiryRecords (pure orchestration, fake Surface injected) ───────
|
|
115
179
|
|
|
116
180
|
test("buildGateInquiryRecords: resolves each claim through the injected Surface and tags calibration", () => {
|