@aiwg/cli 2026.9.9 → 2026.9.10
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/bin/aiwg.mjs +55 -10
- package/dist/src/artifacts/cli.js +20 -8
- package/dist/src/artifacts/corpus-views/taxonomies.js +1 -1
- package/dist/src/cli/handlers/help.js +6 -5
- package/dist/src/cli/handlers/init.js +7 -2
- package/dist/src/cli/handlers/ralph.js +8 -8
- package/dist/src/cli/handlers/steward.js +9 -1
- package/dist/src/cli/handlers/use.js +4 -0
- package/dist/src/cli/handlers/version.js +13 -4
- package/dist/src/cli/services/deployment-verification.js +32 -4
- package/dist/src/cli/watch-service.js +43 -3
- package/dist/src/dataset/fortemi-capability.d.ts +70 -0
- package/dist/src/dataset/fortemi-capability.js +211 -0
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +9 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +49 -8
- package/dist/src/dataset/fortemi-run-receipt.d.ts +1 -0
- package/dist/src/dataset/fortemi-run-receipt.js +2 -19
- package/dist/src/dataset/fortemi-schema-path.d.ts +7 -0
- package/dist/src/dataset/fortemi-schema-path.js +24 -0
- package/dist/src/dataset/index.d.ts +1 -0
- package/dist/src/dataset/index.js +1 -0
- package/dist/src/extensions/project-local-doctor.js +15 -1
- package/dist/src/lint/cli.js +5 -1
- package/dist/src/lint/reporters.js +17 -0
- package/dist/src/lint/runner.js +160 -5
- package/dist/src/policy/authorization.js +1 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +102 -63
- package/package.json +1 -1
- package/schemas/dataset/fortemi-capability-validation/1.0.1/authority.json +61 -0
- package/schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json +469 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +2 -1
- package/tools/agents/deploy-agents.mjs +4 -0
- package/tools/agents/providers/base.mjs +160 -12
- package/tools/agents/providers/claude.mjs +61 -4
- package/tools/agents/providers/codex.mjs +9 -0
- package/tools/skills/deploy-skills-codex.mjs +121 -7
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
3
|
+
import { fortemiPinnedSchemaPath } from "./fortemi-schema-path.js";
|
|
4
|
+
/**
|
|
5
|
+
* Independent AIWG implementation of the Fortemi dataset capability wire rules
|
|
6
|
+
* (Fortemi/fortemi-react#422 validation revision 1.0.1). AIWG pins the authority
|
|
7
|
+
* schema and vectors but never imports the producer's validator, so agreement is
|
|
8
|
+
* established by shared vectors rather than by shared code.
|
|
9
|
+
*/
|
|
10
|
+
export const FORTEMI_CAPABILITY_CONTRACT = "fortemi.dataset-execution-capabilities/v1";
|
|
11
|
+
export const FORTEMI_CAPABILITY_SCHEMA_MAJOR = 1;
|
|
12
|
+
const schema = JSON.parse(readFileSync(fortemiPinnedSchemaPath("schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json"), "utf8"));
|
|
13
|
+
const ajv = new Ajv2020({ strict: true, allErrors: false });
|
|
14
|
+
ajv.addSchema(schema);
|
|
15
|
+
const descriptorStructure = ajv.compile({ $ref: `${schema.$id}#/$defs/descriptor` });
|
|
16
|
+
const requestStructure = ajv.compile({ $ref: `${schema.$id}#/$defs/request` });
|
|
17
|
+
const versionPattern = new RegExp(schema.$defs.semver.pattern);
|
|
18
|
+
const CAPABILITY_IDS = new Set(schema.$defs.capabilityId.enum);
|
|
19
|
+
const LIMIT_KEYS = ["maxInputBytes", "maxRecordBytes", "maxBatchRecords", "maxConcurrency", "maxPageSize", "maxTraversalDepth"];
|
|
20
|
+
const REQUIRED_TOGETHER = [
|
|
21
|
+
["ingest.incremental", ["identity.stable-revision", "checkpoint.read", "checkpoint.write"]],
|
|
22
|
+
["lineage.field", ["lineage.relationship-evidence"]],
|
|
23
|
+
["mutation.reconcile", ["mutation.upsert", "mutation.tombstone"]],
|
|
24
|
+
["index.hybrid", ["index.lexical", "index.vector"]],
|
|
25
|
+
];
|
|
26
|
+
function parseVersion(value) {
|
|
27
|
+
if (typeof value !== "string" || value.length > schema.$defs.semver.maxLength)
|
|
28
|
+
return null;
|
|
29
|
+
const match = versionPattern.exec(value);
|
|
30
|
+
if (!match || match[0] !== value)
|
|
31
|
+
return null;
|
|
32
|
+
const core = match.slice(1, 4).map(Number);
|
|
33
|
+
if (!core.every(Number.isSafeInteger))
|
|
34
|
+
return null;
|
|
35
|
+
return { core, prerelease: match[4]?.split(".") ?? [] };
|
|
36
|
+
}
|
|
37
|
+
/** SemVer 2.0.0 precedence; build metadata is ignored. `null` means unparseable. */
|
|
38
|
+
export function compareFortemiCapabilityVersions(left, right) {
|
|
39
|
+
const a = parseVersion(left);
|
|
40
|
+
const b = parseVersion(right);
|
|
41
|
+
if (!a || !b)
|
|
42
|
+
return null;
|
|
43
|
+
for (let index = 0; index < 3; index++) {
|
|
44
|
+
if (a.core[index] !== b.core[index])
|
|
45
|
+
return a.core[index] > b.core[index] ? 1 : -1;
|
|
46
|
+
}
|
|
47
|
+
if (!a.prerelease.length || !b.prerelease.length) {
|
|
48
|
+
return a.prerelease.length ? -1 : b.prerelease.length ? 1 : 0;
|
|
49
|
+
}
|
|
50
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
|
|
51
|
+
const leftPart = a.prerelease[index];
|
|
52
|
+
const rightPart = b.prerelease[index];
|
|
53
|
+
if (leftPart === rightPart)
|
|
54
|
+
continue;
|
|
55
|
+
if (leftPart === undefined)
|
|
56
|
+
return -1;
|
|
57
|
+
if (rightPart === undefined)
|
|
58
|
+
return 1;
|
|
59
|
+
const leftNumeric = /^[0-9]+$/.test(leftPart);
|
|
60
|
+
const rightNumeric = /^[0-9]+$/.test(rightPart);
|
|
61
|
+
if (leftNumeric !== rightNumeric)
|
|
62
|
+
return leftNumeric ? -1 : 1;
|
|
63
|
+
// Numeric prerelease identifiers can exceed Number's exact integer range.
|
|
64
|
+
if (leftNumeric && leftPart.length !== rightPart.length)
|
|
65
|
+
return leftPart.length > rightPart.length ? 1 : -1;
|
|
66
|
+
return leftPart > rightPart ? 1 : -1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
function majorVersion(value) {
|
|
71
|
+
const parsed = parseVersion(value);
|
|
72
|
+
return parsed && parsed.prerelease.length === 0 ? parsed.core[0] : null;
|
|
73
|
+
}
|
|
74
|
+
function supported(capability) {
|
|
75
|
+
return capability !== undefined && (capability.status === "supported" || capability.status === "experimental");
|
|
76
|
+
}
|
|
77
|
+
/** Structural and semantic validation of an advertised capability descriptor. */
|
|
78
|
+
export function validateFortemiCapabilityDescriptor(descriptor) {
|
|
79
|
+
if (!descriptorStructure(descriptor)) {
|
|
80
|
+
const contract = descriptor?.contract;
|
|
81
|
+
return contract !== undefined && contract !== FORTEMI_CAPABILITY_CONTRACT
|
|
82
|
+
? ["CONTRACT_MAJOR_UNSUPPORTED:/contract"]
|
|
83
|
+
: ["DESCRIPTOR_INVALID:/structure"];
|
|
84
|
+
}
|
|
85
|
+
const diagnostics = [];
|
|
86
|
+
if (descriptor.contract !== FORTEMI_CAPABILITY_CONTRACT)
|
|
87
|
+
diagnostics.push("CONTRACT_MAJOR_UNSUPPORTED:/contract");
|
|
88
|
+
if (majorVersion(descriptor.schemaVersion) !== FORTEMI_CAPABILITY_SCHEMA_MAJOR)
|
|
89
|
+
diagnostics.push("SCHEMA_VERSION_UNSUPPORTED:/schemaVersion");
|
|
90
|
+
if (!parseVersion(descriptor.runtime.version))
|
|
91
|
+
diagnostics.push("DESCRIPTOR_INVALID:/runtime/version");
|
|
92
|
+
const declared = new Map();
|
|
93
|
+
descriptor.capabilities.forEach((capability, index) => {
|
|
94
|
+
if (!CAPABILITY_IDS.has(capability.id) || !parseVersion(capability.version)) {
|
|
95
|
+
diagnostics.push(`DESCRIPTOR_INVALID:/capabilities/${index}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (declared.has(capability.id))
|
|
99
|
+
diagnostics.push(`CAPABILITY_DUPLICATE:/capabilities/${index}/id`);
|
|
100
|
+
declared.set(capability.id, capability);
|
|
101
|
+
for (const key of LIMIT_KEYS) {
|
|
102
|
+
const value = capability.limits?.[key];
|
|
103
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
104
|
+
diagnostics.push(`DESCRIPTOR_INVALID:/capabilities/${index}/limits/${key}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (capability.status !== "unsupported" && capability.evidence.length === 0) {
|
|
108
|
+
diagnostics.push(`DESCRIPTOR_INVALID:/capabilities/${index}/evidence`);
|
|
109
|
+
}
|
|
110
|
+
for (const evidenceId of capability.evidence) {
|
|
111
|
+
if (!descriptor.evidence.some(entry => entry.id === evidenceId)) {
|
|
112
|
+
diagnostics.push(`DESCRIPTOR_INVALID:/capabilities/${index}/evidence`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
for (const [source, requirements] of REQUIRED_TOGETHER) {
|
|
117
|
+
if (!supported(declared.get(source)))
|
|
118
|
+
continue;
|
|
119
|
+
for (const requirement of requirements) {
|
|
120
|
+
if (!supported(declared.get(requirement)))
|
|
121
|
+
diagnostics.push(`CAPABILITY_INCONSISTENT:${source}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (descriptor.guarantees.transaction === "atomic-batch" && !supported(declared.get("transaction.atomic-batch"))) {
|
|
125
|
+
diagnostics.push("CAPABILITY_INCONSISTENT:transaction.atomic-batch");
|
|
126
|
+
}
|
|
127
|
+
if (descriptor.runtime.plane === "static-cache" && descriptor.runtime.dataClass !== "static-cache") {
|
|
128
|
+
diagnostics.push("CAPABILITY_INCONSISTENT:/runtime/dataClass");
|
|
129
|
+
}
|
|
130
|
+
if (descriptor.runtime.plane === "live-remote-persistence" && descriptor.runtime.maturity === "stable"
|
|
131
|
+
&& !descriptor.evidence.some(entry => entry.kind === "live-qualification")) {
|
|
132
|
+
diagnostics.push("CAPABILITY_INCONSISTENT:/runtime/maturity");
|
|
133
|
+
}
|
|
134
|
+
return diagnostics;
|
|
135
|
+
}
|
|
136
|
+
/** Structural and semantic validation of a negotiation request AIWG is about to send. */
|
|
137
|
+
export function validateFortemiCapabilityRequest(request) {
|
|
138
|
+
if (!requestStructure(request)) {
|
|
139
|
+
const contract = request?.contract;
|
|
140
|
+
return contract !== undefined && contract !== FORTEMI_CAPABILITY_CONTRACT
|
|
141
|
+
? ["CONTRACT_MAJOR_UNSUPPORTED:/request/contract"]
|
|
142
|
+
: ["REQUEST_SCHEMA_INVALID:/request"];
|
|
143
|
+
}
|
|
144
|
+
const diagnostics = [];
|
|
145
|
+
for (const [group, requirements] of [["required", request.required], ["optional", request.optional ?? []]]) {
|
|
146
|
+
requirements.forEach((requirement, index) => {
|
|
147
|
+
if (requirement.minimumVersion !== undefined && !parseVersion(requirement.minimumVersion)) {
|
|
148
|
+
diagnostics.push(`DESCRIPTOR_INVALID:/request/${group}/${index}/minimumVersion`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return diagnostics;
|
|
153
|
+
}
|
|
154
|
+
function assess(requirement, declared) {
|
|
155
|
+
const capability = declared.get(requirement.id);
|
|
156
|
+
if (!supported(capability))
|
|
157
|
+
return { ok: false, reason: "unsupported", diagnostic: `REQUIRED_CAPABILITY_MISSING:${requirement.id}` };
|
|
158
|
+
if (requirement.minimumVersion !== undefined) {
|
|
159
|
+
const comparison = compareFortemiCapabilityVersions(capability.version, requirement.minimumVersion);
|
|
160
|
+
if (comparison === null || comparison < 0) {
|
|
161
|
+
return { ok: false, reason: "version-insufficient", diagnostic: `CAPABILITY_VERSION_INSUFFICIENT:${requirement.id}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const key of LIMIT_KEYS) {
|
|
165
|
+
const required = requirement.minimumLimits?.[key];
|
|
166
|
+
if (required !== undefined && (capability.limits?.[key] ?? -1) < required) {
|
|
167
|
+
return { ok: false, reason: "limit-insufficient", diagnostic: `CAPABILITY_LIMIT_INSUFFICIENT:${requirement.id}/${key}` };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Pure negotiation of a request against an advertised descriptor. Performs no
|
|
174
|
+
* I/O and mutates neither input, so a caller can compare the result with the
|
|
175
|
+
* decision a server reports for the same pair.
|
|
176
|
+
*/
|
|
177
|
+
export function negotiateFortemiCapabilities(descriptor, request) {
|
|
178
|
+
const diagnostics = [...validateFortemiCapabilityDescriptor(descriptor), ...validateFortemiCapabilityRequest(request)];
|
|
179
|
+
const selected = [];
|
|
180
|
+
const degradations = [];
|
|
181
|
+
if (diagnostics.length)
|
|
182
|
+
return { accepted: false, selected, degradations, diagnostics };
|
|
183
|
+
const typed = descriptor;
|
|
184
|
+
const negotiation = request;
|
|
185
|
+
const declared = new Map(typed.capabilities.map(capability => [capability.id, capability]));
|
|
186
|
+
for (const requirement of negotiation.required) {
|
|
187
|
+
const assessment = assess(requirement, declared);
|
|
188
|
+
if (assessment.ok)
|
|
189
|
+
selected.push(requirement.id);
|
|
190
|
+
else
|
|
191
|
+
diagnostics.push(assessment.diagnostic);
|
|
192
|
+
}
|
|
193
|
+
for (const requirement of negotiation.optional ?? []) {
|
|
194
|
+
const assessment = assess(requirement, declared);
|
|
195
|
+
if (assessment.ok) {
|
|
196
|
+
selected.push(requirement.id);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const fallback = requirement.fallback?.find(id => supported(declared.get(id)));
|
|
200
|
+
if (fallback)
|
|
201
|
+
selected.push(fallback);
|
|
202
|
+
degradations.push({
|
|
203
|
+
requested: requirement.id,
|
|
204
|
+
...(fallback ? { selected: fallback } : {}),
|
|
205
|
+
reason: assessment.reason,
|
|
206
|
+
changedGuarantees: [fallback ? `${requirement.id} replaced by ${fallback}` : `${requirement.id} omitted`],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return { accepted: diagnostics.length === 0, selected: [...new Set(selected)], degradations, diagnostics };
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=fortemi-capability.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpClientLike } from "../storage/backends/fortemi.js";
|
|
2
|
+
import { type FortemiCapabilityDecision } from "./fortemi-capability.js";
|
|
2
3
|
import { type FortemiDatasetRunReceipt } from "./fortemi-run-receipt.js";
|
|
3
4
|
export declare const FORTEMI_DATASET_EXECUTION_TOOL = "manage_dataset_execution";
|
|
4
5
|
export declare function fortemiDatasetRequestDigest(request: Record<string, unknown>): string;
|
|
@@ -9,10 +10,18 @@ export declare class FortemiDatasetExecutionClient {
|
|
|
9
10
|
constructor(client: McpClientLike);
|
|
10
11
|
private call;
|
|
11
12
|
capabilities(): Promise<Record<string, unknown>>;
|
|
13
|
+
/**
|
|
14
|
+
* Negotiate the request against the advertised descriptor without trusting the
|
|
15
|
+
* server's own decision, then require the two to agree.
|
|
16
|
+
*/
|
|
17
|
+
private negotiate;
|
|
12
18
|
preview(request: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
19
|
+
private previewWithDecision;
|
|
13
20
|
/** The caller must approve the exact digest returned by read-only preview. */
|
|
14
21
|
execute(request: Record<string, unknown>, approvedRequestDigest: string): Promise<FortemiDatasetRunReceipt>;
|
|
15
22
|
private verifyResult;
|
|
23
|
+
/** The decision AIWG negotiated for a run, not the one the server reported. */
|
|
24
|
+
capabilityDecision(runId: string): FortemiCapabilityDecision;
|
|
16
25
|
private knownRun;
|
|
17
26
|
retry(runId: string, action?: "retry" | "resume"): Promise<FortemiDatasetRunReceipt>;
|
|
18
27
|
status(runId: string): Promise<Record<string, unknown>>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FORTEMI_CAPABILITY_CONTRACT, negotiateFortemiCapabilities, validateFortemiCapabilityDescriptor, } from "./fortemi-capability.js";
|
|
1
2
|
import { canonicalFortemiDatasetJson, fortemiDatasetDigest, verifyFortemiDatasetRunReceipt } from "./fortemi-run-receipt.js";
|
|
2
3
|
export const FORTEMI_DATASET_EXECUTION_TOOL = "manage_dataset_execution";
|
|
3
4
|
export function fortemiDatasetRequestDigest(request) {
|
|
@@ -13,6 +14,10 @@ export function fortemiDatasetRequestDigest(request) {
|
|
|
13
14
|
outputSchemaDigest: request.outputSchemaDigest,
|
|
14
15
|
});
|
|
15
16
|
}
|
|
17
|
+
/** Only omission receives the producer's legacy empty-requirement default. */
|
|
18
|
+
function negotiationOf(request) {
|
|
19
|
+
return request.negotiation === undefined ? { contract: FORTEMI_CAPABILITY_CONTRACT, required: [] } : request.negotiation;
|
|
20
|
+
}
|
|
16
21
|
function object(value) {
|
|
17
22
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
18
23
|
throw new Error("CONFORMANCE_FORTEMI_RESPONSE_INVALID");
|
|
@@ -40,20 +45,47 @@ export class FortemiDatasetExecutionClient {
|
|
|
40
45
|
async capabilities() {
|
|
41
46
|
const result = await this.call("capabilities");
|
|
42
47
|
if (object(result.contracts).receipt !== "fortemi.dataset-run-receipt/v1"
|
|
48
|
+
|| object(result.contracts).capability !== FORTEMI_CAPABILITY_CONTRACT
|
|
43
49
|
|| object(result.schemaVersions).receipt !== "1.0.0"
|
|
50
|
+
|| object(result.schemaVersions).capability !== "1.0.0"
|
|
44
51
|
|| object(result.receiptValidation).revision !== "1.0.1"
|
|
45
52
|
|| object(result.receiptValidation).requestBindingRevision !== "1.0.1")
|
|
46
53
|
throw new Error("CONFORMANCE_FORTEMI_RECEIPT_REVISION_UNSUPPORTED");
|
|
54
|
+
const diagnostics = validateFortemiCapabilityDescriptor(result.descriptor);
|
|
55
|
+
if (diagnostics.length)
|
|
56
|
+
throw new Error(`CONFORMANCE_FORTEMI_DESCRIPTOR_INVALID:${diagnostics.join(",")}`);
|
|
47
57
|
return result;
|
|
48
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Negotiate the request against the advertised descriptor without trusting the
|
|
61
|
+
* server's own decision, then require the two to agree.
|
|
62
|
+
*/
|
|
63
|
+
async negotiate(request) {
|
|
64
|
+
const advertised = await this.capabilities();
|
|
65
|
+
const decision = negotiateFortemiCapabilities(advertised.descriptor, negotiationOf(request));
|
|
66
|
+
if (!decision.accepted)
|
|
67
|
+
throw new Error(`CONFORMANCE_FORTEMI_CAPABILITY_UNSATISFIED:${decision.diagnostics.join(",")}`);
|
|
68
|
+
return { descriptor: advertised.descriptor, decision };
|
|
69
|
+
}
|
|
49
70
|
async preview(request) {
|
|
50
|
-
await this.
|
|
71
|
+
return (await this.previewWithDecision(request)).result;
|
|
72
|
+
}
|
|
73
|
+
async previewWithDecision(request) {
|
|
74
|
+
const { descriptor, decision } = await this.negotiate(request);
|
|
51
75
|
const result = await this.call("preview", { request });
|
|
52
76
|
if (result.noSideEffects !== true)
|
|
53
77
|
throw new Error("CONFORMANCE_FORTEMI_PREVIEW_INVALID");
|
|
78
|
+
const reported = object(result.negotiation);
|
|
79
|
+
if (reported.accepted !== true
|
|
80
|
+
|| reported.runtime === undefined
|
|
81
|
+
|| canonicalFortemiDatasetJson(reported.runtime) !== canonicalFortemiDatasetJson(object(descriptor).runtime)
|
|
82
|
+
|| canonicalFortemiDatasetJson(reported.selected) !== canonicalFortemiDatasetJson(decision.selected)
|
|
83
|
+
|| canonicalFortemiDatasetJson(reported.degradations) !== canonicalFortemiDatasetJson(decision.degradations)) {
|
|
84
|
+
throw new Error("CONFORMANCE_FORTEMI_CAPABILITY_DECISION_MISMATCH");
|
|
85
|
+
}
|
|
54
86
|
if (result.accepted === true && result.requestDigest !== fortemiDatasetRequestDigest(request))
|
|
55
87
|
throw new Error("CONFORMANCE_FORTEMI_PREVIEW_BINDING_MISMATCH");
|
|
56
|
-
return result;
|
|
88
|
+
return { result, decision };
|
|
57
89
|
}
|
|
58
90
|
/** The caller must approve the exact digest returned by read-only preview. */
|
|
59
91
|
async execute(request, approvedRequestDigest) {
|
|
@@ -62,7 +94,7 @@ export class FortemiDatasetExecutionClient {
|
|
|
62
94
|
throw new Error("CONFORMANCE_LIVE_AUTHORIZATION_REQUIRED");
|
|
63
95
|
if (fortemiDatasetRequestDigest(request) !== approvedRequestDigest)
|
|
64
96
|
throw new Error("CONFORMANCE_FORTEMI_PLAN_NOT_APPROVED");
|
|
65
|
-
const preview = await this.
|
|
97
|
+
const { result: preview, decision } = await this.previewWithDecision(request);
|
|
66
98
|
if (preview.accepted !== true || preview.requestDigest !== approvedRequestDigest)
|
|
67
99
|
throw new Error("CONFORMANCE_FORTEMI_PLAN_NOT_APPROVED");
|
|
68
100
|
if (typeof request.runId !== "string")
|
|
@@ -70,11 +102,11 @@ export class FortemiDatasetExecutionClient {
|
|
|
70
102
|
const previous = this.runs.get(request.runId);
|
|
71
103
|
if (previous && previous.digest !== approvedRequestDigest)
|
|
72
104
|
throw new Error("CONFORMANCE_FORTEMI_PLAN_NOT_APPROVED");
|
|
73
|
-
this.runs.set(request.runId, { request: structuredClone(request), digest: approvedRequestDigest });
|
|
105
|
+
this.runs.set(request.runId, { request: structuredClone(request), digest: approvedRequestDigest, decision });
|
|
74
106
|
const result = await this.call("execute", { request });
|
|
75
|
-
return this.verifyResult(result, request, approvedRequestDigest);
|
|
107
|
+
return this.verifyResult(result, request, approvedRequestDigest, decision);
|
|
76
108
|
}
|
|
77
|
-
verifyResult(result, request, approvedRequestDigest) {
|
|
109
|
+
verifyResult(result, request, approvedRequestDigest, decision) {
|
|
78
110
|
const errors = verifyFortemiDatasetRunReceipt(result.receipt);
|
|
79
111
|
if (errors.length)
|
|
80
112
|
throw new Error(`CONFORMANCE_FORTEMI_RECEIPT_INVALID:${errors.join(",")}`);
|
|
@@ -83,6 +115,11 @@ export class FortemiDatasetExecutionClient {
|
|
|
83
115
|
|| receipt.namespaceId !== object(object(request.plan).destination).dataset
|
|
84
116
|
|| receipt.state !== result.state || receipt.verification !== result.verification)
|
|
85
117
|
throw new Error("CONFORMANCE_FORTEMI_RECEIPT_BINDING_MISMATCH");
|
|
118
|
+
if (receipt.capabilityDecision.accepted !== true
|
|
119
|
+
|| canonicalFortemiDatasetJson(receipt.capabilityDecision.selected) !== canonicalFortemiDatasetJson(decision.selected)
|
|
120
|
+
|| canonicalFortemiDatasetJson(receipt.capabilityDecision.degradations) !== canonicalFortemiDatasetJson(decision.degradations)) {
|
|
121
|
+
throw new Error("CONFORMANCE_FORTEMI_CAPABILITY_DECISION_MISMATCH");
|
|
122
|
+
}
|
|
86
123
|
const plan = object(request.plan);
|
|
87
124
|
const batch = object(request.batch);
|
|
88
125
|
if (["planId", "sourceRevision", "mode", "planDigest", "configurationDigest", "transformationDigest"].some(key => receipt.bindings[key] !== plan[key])
|
|
@@ -96,6 +133,10 @@ export class FortemiDatasetExecutionClient {
|
|
|
96
133
|
throw new Error("CONFORMANCE_FORTEMI_RECEIPT_BINDING_MISMATCH");
|
|
97
134
|
return receipt;
|
|
98
135
|
}
|
|
136
|
+
/** The decision AIWG negotiated for a run, not the one the server reported. */
|
|
137
|
+
capabilityDecision(runId) {
|
|
138
|
+
return structuredClone(this.knownRun(runId).decision);
|
|
139
|
+
}
|
|
99
140
|
knownRun(runId) {
|
|
100
141
|
const run = this.runs.get(runId);
|
|
101
142
|
if (!run)
|
|
@@ -104,7 +145,7 @@ export class FortemiDatasetExecutionClient {
|
|
|
104
145
|
}
|
|
105
146
|
async retry(runId, action = "retry") {
|
|
106
147
|
const run = this.knownRun(runId);
|
|
107
|
-
return this.verifyResult(await this.call(action, { runId }), run.request, run.digest);
|
|
148
|
+
return this.verifyResult(await this.call(action, { runId }), run.request, run.digest, run.decision);
|
|
108
149
|
}
|
|
109
150
|
async status(runId) {
|
|
110
151
|
const run = this.knownRun(runId);
|
|
@@ -112,7 +153,7 @@ export class FortemiDatasetExecutionClient {
|
|
|
112
153
|
if (result.runId !== runId)
|
|
113
154
|
throw new Error("CONFORMANCE_FORTEMI_RECEIPT_BINDING_MISMATCH");
|
|
114
155
|
if (result.receipt)
|
|
115
|
-
this.verifyResult(result, run.request, run.digest);
|
|
156
|
+
this.verifyResult(result, run.request, run.digest, run.decision);
|
|
116
157
|
else if (result.state !== "running" || result.verification !== "pending")
|
|
117
158
|
throw new Error("CONFORMANCE_FORTEMI_RESPONSE_INVALID");
|
|
118
159
|
return result;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
3
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
6
4
|
import addFormats from "ajv-formats";
|
|
5
|
+
import { fortemiPinnedSchemaPath } from "./fortemi-schema-path.js";
|
|
7
6
|
/** Independent implementation of the Fortemi wire canonicalization contract. */
|
|
8
7
|
export function canonicalFortemiDatasetJson(value) {
|
|
9
8
|
if (value === null)
|
|
@@ -23,25 +22,9 @@ export function canonicalFortemiDatasetJson(value) {
|
|
|
23
22
|
export function fortemiDatasetDigest(value) {
|
|
24
23
|
return `sha256:${createHash("sha256").update(canonicalFortemiDatasetJson(value), "utf8").digest("hex")}`;
|
|
25
24
|
}
|
|
26
|
-
function schemaPath() {
|
|
27
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
for (;;) {
|
|
29
|
-
try {
|
|
30
|
-
const metadata = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
31
|
-
if (metadata.name === "aiwg" || metadata.name === "@aiwg/cli") {
|
|
32
|
-
return join(directory, "schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch { /* Source and built installations have different depths. */ }
|
|
36
|
-
const parent = dirname(directory);
|
|
37
|
-
if (parent === directory)
|
|
38
|
-
throw new Error("FORTEMI_RECEIPT_SCHEMA_UNAVAILABLE");
|
|
39
|
-
directory = parent;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
25
|
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
43
26
|
addFormats(ajv);
|
|
44
|
-
const validate = ajv.compile(JSON.parse(readFileSync(
|
|
27
|
+
const validate = ajv.compile(JSON.parse(readFileSync(fortemiPinnedSchemaPath("schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json"), "utf8")));
|
|
45
28
|
/** Schema validity, semantic consistency, and checksum integrity; not issuer authentication. */
|
|
46
29
|
export function verifyFortemiDatasetRunReceipt(value) {
|
|
47
30
|
if (!validate(value))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a pinned external contract file shipped under `schemas/`.
|
|
3
|
+
* Source and built installations sit at different depths, so walk up to the
|
|
4
|
+
* package root instead of assuming a relative offset.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fortemiPinnedSchemaPath(relativePath: string): string;
|
|
7
|
+
//# sourceMappingURL=fortemi-schema-path.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a pinned external contract file shipped under `schemas/`.
|
|
6
|
+
* Source and built installations sit at different depths, so walk up to the
|
|
7
|
+
* package root instead of assuming a relative offset.
|
|
8
|
+
*/
|
|
9
|
+
export function fortemiPinnedSchemaPath(relativePath) {
|
|
10
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
for (;;) {
|
|
12
|
+
try {
|
|
13
|
+
const metadata = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
14
|
+
if (metadata.name === "aiwg" || metadata.name === "@aiwg/cli")
|
|
15
|
+
return join(directory, relativePath);
|
|
16
|
+
}
|
|
17
|
+
catch { /* Not the package root; keep walking. */ }
|
|
18
|
+
const parent = dirname(directory);
|
|
19
|
+
if (parent === directory)
|
|
20
|
+
throw new Error("FORTEMI_PINNED_SCHEMA_UNAVAILABLE");
|
|
21
|
+
directory = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=fortemi-schema-path.js.map
|
|
@@ -13,6 +13,7 @@ export * from './orchestration-types.js';
|
|
|
13
13
|
export * from './orchestration-repository.js';
|
|
14
14
|
export * from './file-orchestration-repository.js';
|
|
15
15
|
export * from './local-execution-backend.js';
|
|
16
|
+
export * from './fortemi-capability.js';
|
|
16
17
|
export * from './fortemi-execution-bridge.js';
|
|
17
18
|
export * from './fortemi-live-qualification.js';
|
|
18
19
|
export * from './fortemi-run-receipt.js';
|
|
@@ -13,6 +13,7 @@ export * from './orchestration-types.js';
|
|
|
13
13
|
export * from './orchestration-repository.js';
|
|
14
14
|
export * from './file-orchestration-repository.js';
|
|
15
15
|
export * from './local-execution-backend.js';
|
|
16
|
+
export * from './fortemi-capability.js';
|
|
16
17
|
export * from './fortemi-execution-bridge.js';
|
|
17
18
|
export * from './fortemi-live-qualification.js';
|
|
18
19
|
export * from './fortemi-run-receipt.js';
|
|
@@ -173,7 +173,21 @@ export async function buildProjectLocalDoctorSection(opts) {
|
|
|
173
173
|
// Missing — not drift, deploy is just absent
|
|
174
174
|
continue;
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
// "Differs from source" must be judged against the source, not only
|
|
177
|
+
// against whichever rendering the registry recorded. A bundle can be
|
|
178
|
+
// (re)deployed through a path that writes the source verbatim plus
|
|
179
|
+
// the managed marker after the registry captured a transformed copy;
|
|
180
|
+
// the deployed file then matches its source line-for-line while the
|
|
181
|
+
// recorded hash never will (#2560). Accept the recorded deployed
|
|
182
|
+
// hash, the recorded source hash, or the current source file.
|
|
183
|
+
const accepted = new Set([expectedHash]);
|
|
184
|
+
const recordedSourceHash = entry.artifactHashes?.[sourceRel];
|
|
185
|
+
if (recordedSourceHash)
|
|
186
|
+
accepted.add(recordedSourceHash);
|
|
187
|
+
const currentSourceHash = await hashDeployed(join(bundle.artifactPath, sourceRel));
|
|
188
|
+
if (currentSourceHash)
|
|
189
|
+
accepted.add(currentSourceHash.normalized);
|
|
190
|
+
if (!accepted.has(actualHash.normalized) && !accepted.has(actualHash.raw)) {
|
|
177
191
|
driftCount++;
|
|
178
192
|
driftLines.push(` ✗ ${bundle.id} :: ${sourceRel} @ ${provider} (deployed file differs from source)`);
|
|
179
193
|
}
|
package/dist/src/lint/cli.js
CHANGED
|
@@ -86,6 +86,9 @@ function parseArgs(args) {
|
|
|
86
86
|
else if (arg === '--no-recursive') {
|
|
87
87
|
options.recursive = false;
|
|
88
88
|
}
|
|
89
|
+
else if (arg === '--no-gitignore') {
|
|
90
|
+
options.respectGitignore = false;
|
|
91
|
+
}
|
|
89
92
|
else if (!arg.startsWith('-') && !options.target) {
|
|
90
93
|
options.target = arg;
|
|
91
94
|
}
|
|
@@ -123,7 +126,7 @@ export async function main(args) {
|
|
|
123
126
|
}
|
|
124
127
|
// Require a target for actual linting
|
|
125
128
|
if (!options.target) {
|
|
126
|
-
console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--fail-on error|warn|info]');
|
|
129
|
+
console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--fail-on error|warn|info] [--no-gitignore]');
|
|
127
130
|
console.error(' aiwg lint --list-rulesets');
|
|
128
131
|
console.error(' aiwg lint --list-rules <ruleset>');
|
|
129
132
|
process.exitCode = 1;
|
|
@@ -160,6 +163,7 @@ export async function main(args) {
|
|
|
160
163
|
const result = await runLint(targetDir, selectedRulesets, {
|
|
161
164
|
recursive: options.recursive,
|
|
162
165
|
failOn: options.failOn,
|
|
166
|
+
respectGitignore: options.respectGitignore ?? true,
|
|
163
167
|
});
|
|
164
168
|
// Output results
|
|
165
169
|
console.log(formatResult(result, options.format || 'full'));
|
|
@@ -37,7 +37,20 @@ function formatFull(result) {
|
|
|
37
37
|
lines.push(`Target: ${result.target}`);
|
|
38
38
|
lines.push(`Rulesets: ${result.rulesets.join(', ')}`);
|
|
39
39
|
lines.push(`Files checked: ${result.summary.filesChecked}`);
|
|
40
|
+
lines.push(`Rules applied: ${result.summary.rulesApplied} of ${result.summary.rulesSelected}`);
|
|
40
41
|
lines.push('');
|
|
42
|
+
// A target that excludes every rule produces the same clean output as a
|
|
43
|
+
// target that passes them, which is how 34 findings stayed invisible across a
|
|
44
|
+
// long session (#2555). Say so before the verdict.
|
|
45
|
+
if (result.summary.rulesSelected > 0 && result.summary.rulesApplied === 0) {
|
|
46
|
+
lines.push(`\x1b[33m! No rule applies to this target — 0 of ${result.summary.rulesSelected} selected rules matched any file here.\x1b[0m`);
|
|
47
|
+
lines.push(` This is not a clean result. Lint a parent directory (the rules' globs are written from the project root), or check --ruleset.`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
else if (result.summary.inapplicableRules.length > 0) {
|
|
51
|
+
lines.push(`\x1b[33m! ${result.summary.inapplicableRules.length} rule(s) matched no file here: ${result.summary.inapplicableRules.join(', ')}\x1b[0m`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
41
54
|
if (result.diagnostics.length === 0) {
|
|
42
55
|
lines.push('\x1b[32m\u2714 No issues found\x1b[0m');
|
|
43
56
|
}
|
|
@@ -81,6 +94,10 @@ function formatFull(result) {
|
|
|
81
94
|
function formatSummary(result) {
|
|
82
95
|
const lines = [];
|
|
83
96
|
lines.push(`Lint: ${result.summary.filesChecked} files, ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.infos} info`);
|
|
97
|
+
lines.push(`Rules applied: ${result.summary.rulesApplied} of ${result.summary.rulesSelected}`);
|
|
98
|
+
if (result.summary.rulesSelected > 0 && result.summary.rulesApplied === 0) {
|
|
99
|
+
lines.push(`! No rule applies to this target — this is not a clean result (#2555).`);
|
|
100
|
+
}
|
|
84
101
|
const statusIcon = result.summary.passed ? '\u2714 PASS' : '\u2718 FAIL';
|
|
85
102
|
lines.push(`Result: ${statusIcon}`);
|
|
86
103
|
return lines.join('\n');
|