@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.
Files changed (36) hide show
  1. package/bin/aiwg.mjs +55 -10
  2. package/dist/src/artifacts/cli.js +20 -8
  3. package/dist/src/artifacts/corpus-views/taxonomies.js +1 -1
  4. package/dist/src/cli/handlers/help.js +6 -5
  5. package/dist/src/cli/handlers/init.js +7 -2
  6. package/dist/src/cli/handlers/ralph.js +8 -8
  7. package/dist/src/cli/handlers/steward.js +9 -1
  8. package/dist/src/cli/handlers/use.js +4 -0
  9. package/dist/src/cli/handlers/version.js +13 -4
  10. package/dist/src/cli/services/deployment-verification.js +32 -4
  11. package/dist/src/cli/watch-service.js +43 -3
  12. package/dist/src/dataset/fortemi-capability.d.ts +70 -0
  13. package/dist/src/dataset/fortemi-capability.js +211 -0
  14. package/dist/src/dataset/fortemi-dataset-execution.d.ts +9 -0
  15. package/dist/src/dataset/fortemi-dataset-execution.js +49 -8
  16. package/dist/src/dataset/fortemi-run-receipt.d.ts +1 -0
  17. package/dist/src/dataset/fortemi-run-receipt.js +2 -19
  18. package/dist/src/dataset/fortemi-schema-path.d.ts +7 -0
  19. package/dist/src/dataset/fortemi-schema-path.js +24 -0
  20. package/dist/src/dataset/index.d.ts +1 -0
  21. package/dist/src/dataset/index.js +1 -0
  22. package/dist/src/extensions/project-local-doctor.js +15 -1
  23. package/dist/src/lint/cli.js +5 -1
  24. package/dist/src/lint/reporters.js +17 -0
  25. package/dist/src/lint/runner.js +160 -5
  26. package/dist/src/policy/authorization.js +1 -1
  27. package/dist/src/smiths/context-pipeline/workspace-context.js +102 -63
  28. package/package.json +1 -1
  29. package/schemas/dataset/fortemi-capability-validation/1.0.1/authority.json +61 -0
  30. package/schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json +469 -0
  31. package/schemas/security/threat-assessment-report.v1.schema.json +2 -1
  32. package/tools/agents/deploy-agents.mjs +4 -0
  33. package/tools/agents/providers/base.mjs +160 -12
  34. package/tools/agents/providers/claude.mjs +61 -4
  35. package/tools/agents/providers/codex.mjs +9 -0
  36. 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.capabilities();
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.preview(request);
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;
@@ -29,6 +29,7 @@ export interface FortemiDatasetRunReceipt {
29
29
  after: Checkpoint;
30
30
  };
31
31
  capabilityDecision: {
32
+ accepted: boolean;
32
33
  selected: string[];
33
34
  degradations: unknown[];
34
35
  };
@@ -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(schemaPath(), "utf8")));
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
- if (actualHash.normalized !== expectedHash && actualHash.raw !== expectedHash) {
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
  }
@@ -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');