@kontourai/flow-agents 1.2.0 → 1.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/ci.yml +6 -1
- package/.github/workflows/kit-gates-demo.yml +6 -2
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +30 -0
- package/agents/dev.json +1 -1
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/console-learning-projection.d.ts +1 -0
- package/build/src/cli/effective-backlog-settings.d.ts +1 -0
- package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
- package/build/src/cli/init.d.ts +17 -0
- package/build/src/cli/kit.d.ts +1 -0
- package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
- package/build/src/cli/publish-change-helper.d.ts +1 -0
- package/build/src/cli/pull-work-provider.d.ts +1 -0
- package/build/src/cli/runtime-adapter.d.ts +1 -0
- package/build/src/cli/telemetry-doctor.d.ts +1 -0
- package/build/src/cli/usage-feedback.d.ts +1 -0
- package/build/src/cli/utterance-check.d.ts +1 -0
- package/build/src/cli/validate-hook-influence.d.ts +1 -0
- package/build/src/cli/validate-source-tree.d.ts +1 -0
- package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
- package/build/src/cli/veritas-governance.d.ts +1 -0
- package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
- package/build/src/cli/workflow-sidecar.d.ts +32 -0
- package/build/src/cli/workflow-sidecar.js +119 -22
- package/build/src/cli.d.ts +2 -0
- package/build/src/flow-kit/validate.d.ts +81 -0
- package/build/src/flow-kit/validate.js +32 -1
- package/build/src/index.d.ts +5 -0
- package/build/src/index.js +36 -0
- package/build/src/lib/args.d.ts +8 -0
- package/build/src/lib/fs.d.ts +7 -0
- package/build/src/lib/workflow-learning-projection.d.ts +132 -0
- package/build/src/runtime-adapters.d.ts +18 -0
- package/build/src/tools/build-universal-bundles.d.ts +2 -0
- package/build/src/tools/build-universal-bundles.js +14 -0
- package/build/src/tools/common.d.ts +9 -0
- package/build/src/tools/filter-installed-packs.d.ts +2 -0
- package/build/src/tools/generate-context-map.d.ts +2 -0
- package/build/src/tools/validate-package.d.ts +2 -0
- package/build/src/tools/validate-source-tree.d.ts +2 -0
- package/console.telemetry.json +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
- package/docs/developer-architecture.md +14 -0
- package/docs/kit-authoring-guide.md +99 -6
- package/docs/operating-layers.md +2 -2
- package/docs/spec/runtime-hook-surface.md +16 -1
- package/docs/veritas-integration.md +4 -4
- package/docs/workflow-eval-strategy.md +2 -2
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/acceptance/test_opencode_harness.sh +18 -10
- package/evals/acceptance/test_pi_harness.sh +10 -6
- package/evals/ci/run-baseline.sh +1 -1
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
- package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
- package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
- package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/provider-absent.json +2 -2
- package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_goal_fit_hook.sh +144 -0
- package/evals/integration/test_hook_category_behaviors.sh +14 -0
- package/evals/integration/test_kit_conformance_levels.sh +55 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
- package/evals/run.sh +2 -0
- package/evals/static/test_library_exports.sh +85 -0
- package/evals/static/test_package.sh +3 -3
- package/evals/static/test_universal_bundles.sh +15 -0
- package/evals/static/test_workflow_skills.sh +4 -4
- package/kits/builder/flows/build.flow.json +48 -48
- package/kits/builder/flows/shape.flow.json +36 -36
- package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
- package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
- package/kits/knowledge/flows/compile.flow.json +12 -12
- package/kits/knowledge/flows/consolidate.flow.json +16 -16
- package/kits/knowledge/flows/ingest.flow.json +12 -12
- package/kits/knowledge/flows/retire.flow.json +16 -16
- package/kits/knowledge/flows/store-contract.flow.json +12 -12
- package/kits/knowledge/flows/synthesize.flow.json +16 -16
- package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
- package/package.json +14 -2
- package/schemas/workflow-evidence.schema.json +2 -1
- package/scripts/hooks/stop-goal-fit.js +66 -18
- package/src/cli/workflow-sidecar.ts +101 -21
- package/src/flow-kit/validate.ts +55 -1
- package/src/index.ts +53 -0
- package/src/tools/build-universal-bundles.ts +14 -0
- package/tsconfig.json +1 -0
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"expects": [
|
|
15
15
|
{
|
|
16
16
|
"id": "similar-sources-found",
|
|
17
|
-
"kind": "
|
|
17
|
+
"kind": "trust.bundle",
|
|
18
18
|
"required": true,
|
|
19
19
|
"description": "At least one compiled record similar to the target concept has been identified via the similarity detector. Similarity v1 uses category match + link-overlap heuristic. The result is a cluster of source record IDs to synthesize from.",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"bundle_claim": {
|
|
21
|
+
"claimType": "knowledge.synthesize.cluster",
|
|
22
|
+
"subjectType": "artifact",
|
|
23
23
|
"accepted_statuses": ["trusted", "accepted"]
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"expects": [
|
|
31
31
|
{
|
|
32
32
|
"id": "proposal-recorded",
|
|
33
|
-
"kind": "
|
|
33
|
+
"kind": "trust.bundle",
|
|
34
34
|
"required": true,
|
|
35
35
|
"description": "A proposal carrying source refs (proposer_id + source_ids in evidence) has been recorded via the store propose op. The concept body is NOT modified at this step — only a proposes link and mutation log entry are created.",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"bundle_claim": {
|
|
37
|
+
"claimType": "knowledge.synthesize.proposal",
|
|
38
|
+
"subjectType": "artifact",
|
|
39
39
|
"accepted_statuses": ["trusted", "accepted"]
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"expects": [
|
|
47
47
|
{
|
|
48
48
|
"id": "proposal-carries-source-refs",
|
|
49
|
-
"kind": "
|
|
49
|
+
"kind": "trust.bundle",
|
|
50
50
|
"required": true,
|
|
51
51
|
"description": "The proposal evidence includes source_ids referencing every compiled record that contributed to the proposed summary. Gate rejects if source_ids is empty or any referenced record does not exist.",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
52
|
+
"bundle_claim": {
|
|
53
|
+
"claimType": "knowledge.synthesize.evidence",
|
|
54
|
+
"subjectType": "artifact",
|
|
55
55
|
"accepted_statuses": ["trusted", "accepted"]
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"expects": [
|
|
63
63
|
{
|
|
64
64
|
"id": "mutation-gate-decision",
|
|
65
|
-
"kind": "
|
|
65
|
+
"kind": "trust.bundle",
|
|
66
66
|
"required": true,
|
|
67
67
|
"description": "A gate decision (apply or reject) has been recorded. If applied: concept body is updated via the store apply op with rationale and all contributing source_ids in provenance. If rejected: the store reject op is called, the concept body is byte-identical to its pre-proposal state, and only a rejection log entry is appended.",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
68
|
+
"bundle_claim": {
|
|
69
|
+
"claimType": "knowledge.synthesize.gate-decision",
|
|
70
|
+
"subjectType": "artifact",
|
|
71
71
|
"accepted_statuses": ["trusted", "accepted"]
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"expects": [
|
|
14
14
|
{
|
|
15
15
|
"id": "release-claim-present",
|
|
16
|
-
"kind": "
|
|
16
|
+
"kind": "trust.bundle",
|
|
17
17
|
"required": true,
|
|
18
18
|
"description": "A trusted release.evidence claim must be attached before this gate can pass.",
|
|
19
19
|
"explore_hint": "Attach a trust-report JSON file with claim type release.evidence and status trusted.",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"bundle_claim": {
|
|
21
|
+
"claimType": "release.evidence",
|
|
22
22
|
"accepted_statuses": [
|
|
23
23
|
"trusted"
|
|
24
24
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -22,6 +22,15 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
|
+
"main": "build/src/index.js",
|
|
26
|
+
"types": "build/src/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./build/src/index.d.ts",
|
|
30
|
+
"import": "./build/src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
25
34
|
"repository": {
|
|
26
35
|
"type": "git",
|
|
27
36
|
"url": "git+https://github.com/kontourai/flow-agents.git"
|
|
@@ -133,6 +142,9 @@
|
|
|
133
142
|
"typescript": "^6.0.3"
|
|
134
143
|
},
|
|
135
144
|
"dependencies": {
|
|
136
|
-
"@kontourai/flow": "
|
|
145
|
+
"@kontourai/flow": "~1.3.0"
|
|
146
|
+
},
|
|
147
|
+
"optionalDependencies": {
|
|
148
|
+
"hachure": "^0.4.0"
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -239,7 +239,8 @@
|
|
|
239
239
|
"properties": {
|
|
240
240
|
"artifact_kind": {
|
|
241
241
|
"type": "string",
|
|
242
|
-
"
|
|
242
|
+
"description": "Hachure-aligned artifact kind. trust.bundle is the canonical value; TrustReport and Trust Snapshot are legacy aliases.",
|
|
243
|
+
"enum": ["trust.bundle", "TrustReport", "Trust Snapshot"]
|
|
243
244
|
},
|
|
244
245
|
"artifact_ref": {
|
|
245
246
|
"type": "string",
|
|
@@ -80,6 +80,15 @@ function hasSidecars(dir) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns true if a line of validator output looks like a validator-environment
|
|
85
|
+
* error (shell/npm error, tsc missing, spawn failure) rather than a real
|
|
86
|
+
* artifact validation message. Environment errors must never block goal-fit.
|
|
87
|
+
*/
|
|
88
|
+
function isEnvironmentError(line) {
|
|
89
|
+
return /tsc[:\s]|command not found|npm ERR!|npm error|ENOENT|EACCES|Cannot find module|node_modules\/.bin|TypeScript version|version conflict|error TS[0-9]/i.test(line);
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
function sidecarValidation(root, artifactDir) {
|
|
84
93
|
const requireSidecars = String(process.env.FLOW_AGENTS_REQUIRE_SIDECARS || '').toLowerCase() === 'true';
|
|
85
94
|
const requireCritique = String(process.env.FLOW_AGENTS_REQUIRE_CRITIQUE || '').toLowerCase() === 'true';
|
|
@@ -88,8 +97,6 @@ function sidecarValidation(root, artifactDir) {
|
|
|
88
97
|
const packageRoot = fs.existsSync(path.join(root, 'package.json'))
|
|
89
98
|
? root
|
|
90
99
|
: path.resolve(__dirname, '..', '..');
|
|
91
|
-
const packageJson = path.join(packageRoot, 'package.json');
|
|
92
|
-
if (!fs.existsSync(packageJson)) return [`${relative(root, artifactDir)} sidecar validation: package.json is missing; cannot run TypeScript workflow validator.`];
|
|
93
100
|
|
|
94
101
|
let sidecarFiles = [];
|
|
95
102
|
try {
|
|
@@ -112,26 +119,67 @@ function sidecarValidation(root, artifactDir) {
|
|
|
112
119
|
|
|
113
120
|
if (sidecarFiles.length === 0) return [];
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// Part 1 fix: invoke the already-built validator directly via `node`, bypassing
|
|
123
|
+
// `npm run build` (tsc). npm-installed packages ship build/ in the package files,
|
|
124
|
+
// so the compiled JS is always available. Only fall back to npm run if build/ is
|
|
125
|
+
// absent (a raw dev checkout that hasn't been built yet).
|
|
126
|
+
const builtValidator = path.join(packageRoot, 'build', 'src', 'cli', 'validate-workflow-artifacts.js');
|
|
127
|
+
const hasBuild = fs.existsSync(builtValidator);
|
|
128
|
+
|
|
129
|
+
const validatorArgs = ['--skip-markdown-validation'];
|
|
130
|
+
if (requireSidecars) validatorArgs.push('--require-sidecars');
|
|
131
|
+
if (requireCritique) validatorArgs.push('--require-critique');
|
|
132
|
+
validatorArgs.push(artifactDir);
|
|
133
|
+
|
|
134
|
+
let result;
|
|
135
|
+
if (hasBuild) {
|
|
136
|
+
// Direct node invocation: no tsc, no npm build step, works from any npm install.
|
|
137
|
+
result = spawnSync(process.execPath, [builtValidator, ...validatorArgs], {
|
|
138
|
+
cwd: packageRoot,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 30000,
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
// Dev checkout without build/: fall back to npm run (may trigger tsc).
|
|
144
|
+
// If this also fails due to environment issues, Part 2 handles it below.
|
|
145
|
+
const npmArgs = ['run', 'workflow:validate-artifacts', '--silent', '--', ...validatorArgs];
|
|
146
|
+
result = spawnSync('npm', npmArgs, {
|
|
147
|
+
cwd: packageRoot,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
timeout: 30000,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
120
152
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
// Part 2 fix: treat validator-environment failures as SKIP, never as blocking.
|
|
154
|
+
// A spawn error (ENOENT, timeout) means the validator couldn't run at all.
|
|
155
|
+
if (result.error) {
|
|
156
|
+
// Validator couldn't be launched — environment issue, not a goal-fit failure.
|
|
157
|
+
return [`${relative(root, artifactDir)} sidecar validation skipped: validator could not run (${result.error.code || result.error.message})`];
|
|
158
|
+
}
|
|
126
159
|
|
|
127
160
|
if (result.status === 0) return [];
|
|
128
|
-
|
|
161
|
+
|
|
162
|
+
// Validator ran and exited non-zero. Separate real validation errors from
|
|
163
|
+
// environment errors (tsc missing, npm ERR!, shell errors) so that a broken
|
|
164
|
+
// validator environment never blocks goal-fit.
|
|
165
|
+
const allLines = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
129
166
|
.split('\n')
|
|
130
167
|
.map(line => line.trim())
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
|
|
170
|
+
const envLines = allLines.filter(isEnvironmentError);
|
|
171
|
+
const validationLines = allLines.filter(line => !isEnvironmentError(line));
|
|
172
|
+
|
|
173
|
+
if (envLines.length > 0 && validationLines.length === 0) {
|
|
174
|
+
// Pure environment failure — skip, do not block.
|
|
175
|
+
return [`${relative(root, artifactDir)} sidecar validation skipped: validator environment error (${envLines[0].slice(0, 120)})`];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Real validation errors (possibly mixed with a few env noise lines).
|
|
179
|
+
const output = validationLines.length > 0 ? validationLines : allLines;
|
|
180
|
+
const trimmed = output.slice(0, 12);
|
|
181
|
+
if (trimmed.length === 0) trimmed.push(`validator exited with status ${result.status ?? 'unknown'}`);
|
|
182
|
+
return trimmed.map(line => `${relative(root, artifactDir)} sidecar validation: ${line}`);
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
function isWorkflowArtifact(artifact) {
|
|
@@ -295,7 +343,7 @@ function analyze(root, now = Date.now()) {
|
|
|
295
343
|
}
|
|
296
344
|
warnings.push(...sidecarGuidance(root, path.dirname(latest.file)));
|
|
297
345
|
|
|
298
|
-
const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation
|
|
346
|
+
const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action/.test(w));
|
|
299
347
|
return { warnings, blocking };
|
|
300
348
|
}
|
|
301
349
|
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
|
|
6
8
|
type AnyObj = Record<string, any>;
|
|
7
9
|
|
|
8
|
-
const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
9
|
-
const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
10
|
-
const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
11
|
-
const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
12
|
-
const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
10
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
11
|
+
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
12
|
+
export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
13
|
+
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
14
|
+
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
13
15
|
|
|
14
16
|
function now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
15
17
|
function read(file: string): string { return fs.readFileSync(file, "utf8"); }
|
|
16
|
-
function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
18
|
+
export function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
17
19
|
function printJson(payload: AnyObj): void { console.log(JSON.stringify(payload).replace(/":/g, '": ').replace(/,"/g, ', "')); }
|
|
18
|
-
function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
19
|
-
function appendJsonl(file: string, payload: AnyObj): void {
|
|
20
|
+
export function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
|
|
21
|
+
export function appendJsonl(file: string, payload: AnyObj): void {
|
|
20
22
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
23
|
const line = JSON.stringify(payload, Object.keys(payload).sort()).replace(/":/g, '": ').replace(/,"/g, ', "');
|
|
22
24
|
fs.appendFileSync(file, `${line}\n`);
|
|
@@ -24,6 +26,60 @@ function appendJsonl(file: string, payload: AnyObj): void {
|
|
|
24
26
|
function die(message: string): never { throw new Error(message); }
|
|
25
27
|
function slugify(value: string, fallback: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
|
|
26
28
|
|
|
29
|
+
// Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
|
|
30
|
+
// Install hachure (^0.4.0) as an optional dependency to enable schema validation.
|
|
31
|
+
function tryLoadHachureValidator(): ((bundle: unknown) => { valid: boolean; errors: string[] }) | null {
|
|
32
|
+
try {
|
|
33
|
+
const _require = createRequire(import.meta.url);
|
|
34
|
+
const hachureDir = path.dirname(_require.resolve("hachure"));
|
|
35
|
+
const schemasDir = path.join(hachureDir, "schemas");
|
|
36
|
+
const Ajv = _require("ajv/dist/2020");
|
|
37
|
+
const schemas: Record<string, any> = {};
|
|
38
|
+
for (const file of fs.readdirSync(schemasDir)) {
|
|
39
|
+
if (!file.endsWith(".schema.json")) continue;
|
|
40
|
+
schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
|
|
41
|
+
}
|
|
42
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
43
|
+
for (const [filename, schema] of Object.entries(schemas)) {
|
|
44
|
+
if (filename === "trust-bundle.schema.json") continue;
|
|
45
|
+
ajv.addSchema(schema, filename);
|
|
46
|
+
}
|
|
47
|
+
const trustBundleSchema = schemas["trust-bundle.schema.json"];
|
|
48
|
+
if (!trustBundleSchema) return null;
|
|
49
|
+
const validate = ajv.compile(trustBundleSchema);
|
|
50
|
+
return (bundle: unknown) => {
|
|
51
|
+
const valid = validate(bundle);
|
|
52
|
+
if (valid) return { valid: true, errors: [] };
|
|
53
|
+
const errors = ((validate as any).errors ?? []).map((err: any) => {
|
|
54
|
+
const loc = err.instancePath || err.schemaPath || "";
|
|
55
|
+
return `${loc} ${err.message ?? "invalid"}`.trim();
|
|
56
|
+
});
|
|
57
|
+
return { valid: false, errors };
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let _hachureValidator: ReturnType<typeof tryLoadHachureValidator> | undefined;
|
|
64
|
+
function getHachureValidator(): ReturnType<typeof tryLoadHachureValidator> {
|
|
65
|
+
if (_hachureValidator === undefined) _hachureValidator = tryLoadHachureValidator();
|
|
66
|
+
return _hachureValidator;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate a Hachure trust.bundle against the canonical trust-bundle schema.
|
|
71
|
+
* Returns `{ valid, errors, available }`. When the optional `hachure` dependency
|
|
72
|
+
* is not installed, validation is unavailable and this returns
|
|
73
|
+
* `{ valid: true, errors: [], available: false }` (fail-open) so callers can
|
|
74
|
+
* choose to treat unvalidated bundles as acceptable or gate on `available`.
|
|
75
|
+
* This is the same validator the sidecar writer uses for trust-backed evidence.
|
|
76
|
+
*/
|
|
77
|
+
export function validateTrustBundle(bundle: unknown): { valid: boolean; errors: string[]; available: boolean } {
|
|
78
|
+
const validate = getHachureValidator();
|
|
79
|
+
if (!validate) return { valid: true, errors: [], available: false };
|
|
80
|
+
return { ...validate(bundle), available: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
27
83
|
function safeRepoIdentifier(value: string): string {
|
|
28
84
|
const trimmed = value.trim().replace(/\.git$/, "");
|
|
29
85
|
if (!trimmed || trimmed.length > 120) return "";
|
|
@@ -68,7 +124,7 @@ function repoIdentifier(): string {
|
|
|
68
124
|
return safeRepoIdentifier(path.basename(process.cwd())) || "workspace";
|
|
69
125
|
}
|
|
70
126
|
|
|
71
|
-
function sidecarBase(slug: string): AnyObj {
|
|
127
|
+
export function sidecarBase(slug: string): AnyObj {
|
|
72
128
|
return { schema_version: "1.0", task_slug: slug, repo: repoIdentifier() };
|
|
73
129
|
}
|
|
74
130
|
|
|
@@ -336,7 +392,7 @@ function hasNonEmptyString(value: unknown): boolean {
|
|
|
336
392
|
function hasPositiveInteger(value: unknown): boolean {
|
|
337
393
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
338
394
|
}
|
|
339
|
-
function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
395
|
+
export function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
340
396
|
if (!["source", "command", "artifact", "provider", "external"].includes(ref.kind)) die(`${label} entry kind must be one of: source, command, artifact, provider, external`);
|
|
341
397
|
for (const key of Object.keys(ref)) if (!["kind", "url", "file", "line_start", "line_end", "excerpt", "summary"].includes(key)) die(`${label} entries contain unsupported field: ${key}`);
|
|
342
398
|
if (ref.url !== undefined && !hasNonEmptyString(ref.url)) die(`${label} entry url must be a non-empty string`);
|
|
@@ -352,7 +408,7 @@ function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
|
|
|
352
408
|
if ((ref.kind === "provider" || ref.kind === "external") && !hasNonEmptyString(ref.url)) die(`${label} ${ref.kind} refs require url`);
|
|
353
409
|
return ref;
|
|
354
410
|
}
|
|
355
|
-
function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
411
|
+
export function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
356
412
|
if (!Array.isArray(raw)) die(`${label} must be an array`);
|
|
357
413
|
return raw.map((ref) => {
|
|
358
414
|
if (typeof ref === "string") die(`${label} entries must be structured evidence reference objects; legacy string refs are not supported`);
|
|
@@ -360,7 +416,7 @@ function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
|
|
|
360
416
|
return validateEvidenceRef({ ...ref as AnyObj }, label);
|
|
361
417
|
});
|
|
362
418
|
}
|
|
363
|
-
function normalizeCheck(raw: AnyObj): AnyObj {
|
|
419
|
+
export function normalizeCheck(raw: AnyObj): AnyObj {
|
|
364
420
|
const check = { ...raw };
|
|
365
421
|
if (!check.id || !check.kind || !check.status || !check.summary) die("check requires id, kind, status, and summary");
|
|
366
422
|
if (!checkKinds.has(check.kind)) die("kind must be one of: build, types, lint, test, security, diff, browser, runtime, policy, external");
|
|
@@ -372,11 +428,27 @@ function normalizeCheck(raw: AnyObj): AnyObj {
|
|
|
372
428
|
}
|
|
373
429
|
function normalizeSurfaceRefs(refs: any): AnyObj[] {
|
|
374
430
|
if (!Array.isArray(refs)) die("surface_trust_refs must be an array");
|
|
431
|
+
const hachureValidate = getHachureValidator();
|
|
375
432
|
return refs.map((ref) => {
|
|
376
433
|
const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
|
|
377
434
|
for (const key of keys.map((k) => k.slice(1, -2))) if (key.toLowerCase().includes("veritas")) die(`unsupported field in Surface trust ref: ${key}`);
|
|
378
435
|
const out = { ...ref };
|
|
379
|
-
|
|
436
|
+
// trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
|
|
437
|
+
if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
|
|
438
|
+
// When hachure is installed, validate the referenced trust artifact if it is a local file
|
|
439
|
+
if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
|
|
440
|
+
try {
|
|
441
|
+
const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
|
|
442
|
+
const result = hachureValidate(bundle);
|
|
443
|
+
if (!result.valid) {
|
|
444
|
+
const errorSummary = result.errors.slice(0, 3).join("; ");
|
|
445
|
+
die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (err instanceof Error && err.message.includes("failed Hachure schema validation")) throw err;
|
|
449
|
+
// File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
|
|
450
|
+
}
|
|
451
|
+
}
|
|
380
452
|
const status = deriveSurfaceStatus(out);
|
|
381
453
|
if (out.status === "pass" && status !== "pass") die("surface_trust_refs contradicts Surface trust facts");
|
|
382
454
|
return out;
|
|
@@ -394,15 +466,16 @@ function surfaceCheckFromArtifact(file: string, index: number): AnyObj {
|
|
|
394
466
|
const lower = JSON.stringify(raw).toLowerCase();
|
|
395
467
|
let ref: AnyObj;
|
|
396
468
|
if (lower.includes("provider") && lower.includes("absent")) {
|
|
397
|
-
ref = { artifact_kind: "
|
|
469
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
|
|
398
470
|
} else if (lower.includes("artifact") && lower.includes("absent")) {
|
|
399
|
-
ref = { artifact_kind: "
|
|
471
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
|
|
400
472
|
} else {
|
|
401
473
|
const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
|
|
402
474
|
const freshness = lower.includes("stale") ? "stale" : "fresh";
|
|
403
475
|
const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
|
|
404
476
|
const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
|
|
405
|
-
|
|
477
|
+
// Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
|
|
478
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "builder.trust.bundle", claim_type: "builder.trust.bundle", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
|
|
406
479
|
ref.status = deriveSurfaceStatus(ref);
|
|
407
480
|
ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
|
|
408
481
|
}
|
|
@@ -426,7 +499,7 @@ function validateAcceptanceEvidenceRefs(dir: string): void {
|
|
|
426
499
|
if (criterion.evidence_refs !== undefined) normalizeEvidenceRefs(criterion.evidence_refs, `acceptance.criteria[${index}].evidence_refs`);
|
|
427
500
|
});
|
|
428
501
|
}
|
|
429
|
-
function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
|
|
502
|
+
export function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
|
|
430
503
|
writeJson(path.join(dir, "state.json"), { ...loadJson(path.join(dir, "state.json")), ...sidecarBase(slug), status, phase, updated_at: timestamp, artifact_paths: relArtifacts(dir), next_action: { status: next, summary } });
|
|
431
504
|
}
|
|
432
505
|
function recordEvidence(p: ReturnType<typeof parseArgs>): number {
|
|
@@ -479,7 +552,7 @@ function advanceState(p: ReturnType<typeof parseArgs>): number {
|
|
|
479
552
|
return 0;
|
|
480
553
|
}
|
|
481
554
|
|
|
482
|
-
function normalizeFinding(raw: AnyObj): AnyObj {
|
|
555
|
+
export function normalizeFinding(raw: AnyObj): AnyObj {
|
|
483
556
|
if (raw.file_refs !== undefined && !Array.isArray(raw.file_refs)) die("file_refs must be an array");
|
|
484
557
|
return raw;
|
|
485
558
|
}
|
|
@@ -536,7 +609,7 @@ function recordRelease(p: ReturnType<typeof parseArgs>): number {
|
|
|
536
609
|
writeState(dir, slug, "delivered", "release", payload.updated_at, stateSummary);
|
|
537
610
|
return 0;
|
|
538
611
|
}
|
|
539
|
-
function validateLearningCorrection(record: AnyObj): void {
|
|
612
|
+
export function validateLearningCorrection(record: AnyObj): void {
|
|
540
613
|
const correction = record.correction;
|
|
541
614
|
if (correction === undefined) return;
|
|
542
615
|
if (!correction || typeof correction !== "object" || Array.isArray(correction)) die("correction must be an object");
|
|
@@ -566,7 +639,7 @@ function validateLearningPrevention(prevention: unknown): void {
|
|
|
566
639
|
if (typeof value.status !== "string" || value.status.length === 0) die("correction.prevention.status is required");
|
|
567
640
|
if (!["open", "completed", "accepted", "deferred", "rejected"].includes(value.status)) die("correction.prevention.status must be one of: open, completed, accepted, deferred, rejected");
|
|
568
641
|
}
|
|
569
|
-
function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
|
|
642
|
+
export function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
|
|
570
643
|
if (!Array.isArray(raw.source_refs)) die("source_refs must be an array");
|
|
571
644
|
if (!Array.isArray(raw.facts)) die("facts must be an array");
|
|
572
645
|
if (!Array.isArray(raw.routing)) die("routing must be an array");
|
|
@@ -673,4 +746,11 @@ async function main(): Promise<number> {
|
|
|
673
746
|
});
|
|
674
747
|
}
|
|
675
748
|
|
|
676
|
-
|
|
749
|
+
// Run the CLI only when executed directly, not when imported as a library.
|
|
750
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
751
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
752
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
753
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
754
|
+
if (_selfRealPath === _argv1RealPath) {
|
|
755
|
+
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
756
|
+
}
|
package/src/flow-kit/validate.ts
CHANGED
|
@@ -24,6 +24,47 @@ export interface KitConformanceLevel {
|
|
|
24
24
|
k2: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
|
|
29
|
+
*
|
|
30
|
+
* - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
|
|
31
|
+
* FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
|
|
32
|
+
* and distributed with the flow-agents package.
|
|
33
|
+
* - "verified": reserved for a future third-party verification process (e.g. self-certification
|
|
34
|
+
* via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
|
|
35
|
+
* - "unverified": default for all kits not in the first-party allowlist. This says nothing about
|
|
36
|
+
* the kit's quality — it only means Kontour has not vouched for it.
|
|
37
|
+
*
|
|
38
|
+
* The v2 path for "verified": cryptographic signing / attestation against the conformance kit
|
|
39
|
+
* and Veritas claims substrate is the natural next step and is intentionally deferred.
|
|
40
|
+
*/
|
|
41
|
+
export type KitTrustLevel = "first-party" | "verified" | "unverified";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
|
|
45
|
+
*
|
|
46
|
+
* Criteria for inclusion:
|
|
47
|
+
* 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
|
|
48
|
+
* 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
|
|
49
|
+
* 3. Kontour owns and maintains the kit's content and release lifecycle.
|
|
50
|
+
*
|
|
51
|
+
* To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
|
|
52
|
+
* Third-party forks or community kits published elsewhere are NOT first-party, even if they
|
|
53
|
+
* share a similar id — first-party is tied to provenance in this specific repository.
|
|
54
|
+
*/
|
|
55
|
+
export const FIRST_PARTY_KIT_IDS: ReadonlySet<string> = new Set(["builder", "knowledge"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive the trust level for a kit id.
|
|
59
|
+
*
|
|
60
|
+
* v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
|
|
61
|
+
* "verified" is reserved for future third-party verification (not yet granted to any kit).
|
|
62
|
+
*/
|
|
63
|
+
export function deriveKitTrust(kitId: string): KitTrustLevel {
|
|
64
|
+
if (FIRST_PARTY_KIT_IDS.has(kitId)) return "first-party";
|
|
65
|
+
return "unverified";
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
export interface KitTargetsResult {
|
|
28
69
|
kit_id: string;
|
|
29
70
|
kit_name: string;
|
|
@@ -32,6 +73,11 @@ export interface KitTargetsResult {
|
|
|
32
73
|
targets: KitTargetConsumer[];
|
|
33
74
|
/** Extension field namespaces that are not Flow or Flow Agents-owned. */
|
|
34
75
|
third_party_extensions: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
|
|
78
|
+
* "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
|
|
79
|
+
*/
|
|
80
|
+
trust: KitTrustLevel;
|
|
35
81
|
}
|
|
36
82
|
|
|
37
83
|
// Lazy-loaded cache for validateKitContainer from @kontourai/flow.
|
|
@@ -83,7 +129,7 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
|
|
|
83
129
|
}
|
|
84
130
|
|
|
85
131
|
/**
|
|
86
|
-
* Derives the consumer-target level (K0/K1/K2)
|
|
132
|
+
* Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
|
|
87
133
|
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
88
134
|
*
|
|
89
135
|
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
@@ -94,6 +140,10 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
|
|
|
94
140
|
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
95
141
|
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
96
142
|
*
|
|
143
|
+
* Trust derivation (from kontourai/flow-agents#79):
|
|
144
|
+
* - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
|
|
145
|
+
* - "unverified": all other kits (default; "verified" is reserved for a future process).
|
|
146
|
+
*
|
|
97
147
|
* @param manifest The kit.json manifest object.
|
|
98
148
|
* @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
|
|
99
149
|
* Pass the real kit directory from `inspect` to get authoritative K0 validation.
|
|
@@ -125,12 +175,16 @@ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir
|
|
|
125
175
|
if (k1) targets.push("flow-agents");
|
|
126
176
|
for (const ns of thirdPartyExtensions) targets.push(ns);
|
|
127
177
|
|
|
178
|
+
// Derive trust level orthogonally to the K-level capability axis.
|
|
179
|
+
const trust = deriveKitTrust(kitId);
|
|
180
|
+
|
|
128
181
|
return {
|
|
129
182
|
kit_id: kitId,
|
|
130
183
|
kit_name: kitName,
|
|
131
184
|
conformance: { k0, k1, k2 },
|
|
132
185
|
targets,
|
|
133
186
|
third_party_extensions: thirdPartyExtensions,
|
|
187
|
+
trust,
|
|
134
188
|
};
|
|
135
189
|
}
|
|
136
190
|
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public library surface for `@kontourai/flow-agents`.
|
|
3
|
+
*
|
|
4
|
+
* Native orchestration hosts can import the canonical workflow-sidecar
|
|
5
|
+
* writer/validator here instead of shelling out to the
|
|
6
|
+
* `flow-agents-workflow-sidecar` CLI or reimplementing validated
|
|
7
|
+
* read / merge / write of workflow evidence. This is the same code the CLI
|
|
8
|
+
* runs — importing it does not execute the CLI.
|
|
9
|
+
*
|
|
10
|
+
* The sidecar JSON Schemas ship under `schemas/` and can be validated against
|
|
11
|
+
* directly; the helpers below are the canonical writer/validator that produce
|
|
12
|
+
* and check conforming artifacts.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
// Trust-bundle (Hachure) validation — the same validator the writer uses.
|
|
21
|
+
validateTrustBundle,
|
|
22
|
+
// Evidence / check / learning validation + normalization. These throw on
|
|
23
|
+
// invalid input (with the same messages the CLI surfaces) and return the
|
|
24
|
+
// normalized object on success.
|
|
25
|
+
normalizeCheck,
|
|
26
|
+
normalizeFinding,
|
|
27
|
+
normalizeLearning,
|
|
28
|
+
normalizeEvidenceRefs,
|
|
29
|
+
validateEvidenceRef,
|
|
30
|
+
validateLearningCorrection,
|
|
31
|
+
// Sidecar read / merge / write primitives.
|
|
32
|
+
loadJson,
|
|
33
|
+
writeJson,
|
|
34
|
+
appendJsonl,
|
|
35
|
+
sidecarBase,
|
|
36
|
+
writeState,
|
|
37
|
+
// Schema vocabularies (the allowed status/phase/kind values).
|
|
38
|
+
statuses,
|
|
39
|
+
phases,
|
|
40
|
+
checkKinds,
|
|
41
|
+
checkStatuses,
|
|
42
|
+
verdicts,
|
|
43
|
+
} from "./cli/workflow-sidecar.js";
|
|
44
|
+
|
|
45
|
+
/** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
|
|
46
|
+
export function readSidecar(dir: string, name: string): Record<string, any> {
|
|
47
|
+
return _loadJson(path.join(dir, name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Write a sidecar JSON file into a workflow artifact directory (pretty-printed, trailing newline). */
|
|
51
|
+
export function writeSidecar(dir: string, name: string, payload: Record<string, any>): void {
|
|
52
|
+
_writeJson(path.join(dir, name), payload);
|
|
53
|
+
}
|