@aiwg/cli 2026.9.3 → 2026.9.5
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/README.md +21 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/agentic/code/providers/capability-matrix.yaml +45 -3
- package/agentic/code/providers/deepseek-harness/README.md +8 -0
- package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/bin/aiwg.mjs +1 -0
- package/dist/src/api/index.d.ts +14 -0
- package/dist/src/api/index.js +14 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
- package/dist/src/artifacts/index-files.js +17 -2
- package/dist/src/artifacts/repair.js +55 -6
- package/dist/src/catalog/cli.js +21 -7
- package/dist/src/catalog/cli.mjs +22 -7
- package/dist/src/cli/agent-spawn.js +10 -1
- package/dist/src/cli/handlers/artifacts.js +22 -3
- package/dist/src/cli/handlers/help.js +3 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/models.js +2 -2
- package/dist/src/cli/handlers/output-mode.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +1 -1
- package/dist/src/cli/handlers/sessions.js +55 -15
- package/dist/src/cli/handlers/steward.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +5 -0
- package/dist/src/cli/handlers/use.js +37 -0
- package/dist/src/cli/handlers/writer-profile.js +110 -0
- package/dist/src/cli/handlers/writing.js +122 -0
- package/dist/src/cli/router.js +5 -1
- package/dist/src/config/project-artifacts-runtime.mjs +33 -1
- package/dist/src/config/project-artifacts.js +1 -1
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
- package/dist/src/dataset/fortemi-live-qualification.js +20 -21
- package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
- package/dist/src/dataset/fortemi-run-receipt.js +74 -0
- package/dist/src/dataset/index.d.ts +2 -0
- package/dist/src/dataset/index.js +2 -0
- package/dist/src/extensions/commands/definitions.js +24 -0
- package/dist/src/extensions/manifest.js +3 -0
- package/dist/src/mcp/server.mjs +2 -0
- package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/network-analysis/analyzer.js +667 -0
- package/dist/src/network-analysis/citations.js +107 -0
- package/dist/src/network-analysis/forensics.js +132 -0
- package/dist/src/network-analysis/governance.js +216 -0
- package/dist/src/network-analysis/index.js +10 -0
- package/dist/src/network-analysis/probe.js +405 -0
- package/dist/src/network-analysis/recipes.js +88 -0
- package/dist/src/network-analysis/research.js +181 -0
- package/dist/src/network-analysis/termshark.js +252 -0
- package/dist/src/network-analysis/verification.js +171 -0
- package/dist/src/output-modes/registry.js +37 -6
- package/dist/src/output-modes/runtime.js +164 -28
- package/dist/src/providers/capability-matrix.yaml +45 -3
- package/dist/src/providers/provider-definitions.js +49 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/providers/transformation-receipt.js +3 -2
- package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +2 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +5 -1
- package/dist/src/skills/deployer.js +6 -6
- package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
- package/dist/src/writing/channel-packs.js +13 -0
- package/dist/src/writing/contextual-diagnostics.js +142 -0
- package/dist/src/writing/example-generator.js +7 -6
- package/dist/src/writing/exemplar-selection.js +186 -0
- package/dist/src/writing/fidelity.js +61 -0
- package/dist/src/writing/validation-engine.js +32 -15
- package/dist/src/writing/voice-evaluation.js +301 -0
- package/dist/src/writing/voice-revision.js +201 -0
- package/dist/src/writing/writer-migration.js +216 -0
- package/dist/src/writing/writer-profile-legacy.js +145 -0
- package/dist/src/writing/writer-profile-store.js +117 -0
- package/dist/src/writing/writer-profile.js +222 -0
- package/dist/src/writing/writing-brief.js +166 -0
- package/dist/src/writing/writing-channels.js +63 -0
- package/dist/src/writing/writing-consumer.js +39 -0
- package/dist/src/writing/writing-receipt.js +266 -0
- package/package.json +1 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
- package/tools/agents/deploy-agents.mjs +7 -3
- package/tools/agents/providers/antigravity.mjs +1 -1
- package/tools/agents/providers/base.mjs +3 -2
- package/tools/agents/providers/deepseek-harness.mjs +66 -0
- package/tools/agents/providers/hermes.mjs +1 -1
- package/tools/agents/providers/openhuman.mjs +2 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { lstat, mkdir, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import semver from 'semver';
|
|
6
|
+
import { formatEvidenceCitation } from './citations.js';
|
|
7
|
+
import { hashEvidenceFile, safeProcessSpec } from './governance.js';
|
|
8
|
+
export const TERMSHARK_HANDOFF_SCHEMA = 'aiwg.network-analysis.termshark-handoff.v1';
|
|
9
|
+
export function createDefaultTermsharkLaunchHost() {
|
|
10
|
+
return {
|
|
11
|
+
async run(spec, options) {
|
|
12
|
+
const cacheRoot = path.join(options.configRoot, 'cache');
|
|
13
|
+
const dataRoot = path.join(options.configRoot, 'data');
|
|
14
|
+
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
15
|
+
await mkdir(dataRoot, { recursive: true, mode: 0o700 });
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const child = spawn(spec.file, [...spec.args], {
|
|
18
|
+
shell: false,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
signal: options.signal,
|
|
21
|
+
env: {
|
|
22
|
+
PATH: path.dirname(options.tsharkPath),
|
|
23
|
+
XDG_CONFIG_HOME: options.configRoot,
|
|
24
|
+
XDG_CACHE_HOME: cacheRoot,
|
|
25
|
+
XDG_DATA_HOME: dataRoot,
|
|
26
|
+
HOME: options.configRoot,
|
|
27
|
+
TERM: process.env.TERM || 'xterm-256color',
|
|
28
|
+
LC_ALL: 'C',
|
|
29
|
+
LANG: 'C',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
child.once('error', reject);
|
|
33
|
+
child.once('close', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function createTermsharkHandoff(options) {
|
|
39
|
+
validateHandoffOptions(options);
|
|
40
|
+
const context = findAnalysisContext(options.evidence, options.contextDigest);
|
|
41
|
+
if (context.tool?.name !== 'tshark' || context.tool?.executable_path !== options.tsharkPath) {
|
|
42
|
+
throw new Error('Termshark handoff TShark path does not match the evidence analysis context');
|
|
43
|
+
}
|
|
44
|
+
const sourceHash = String(options.evidence.capture.hashes?.source?.value ?? '');
|
|
45
|
+
const actual = await hashEvidenceFile(options.capturePath, 'source-capture');
|
|
46
|
+
if (sourceHash !== actual.value || options.evidence.capture.capture_digest !== `sha256:${actual.value}`) {
|
|
47
|
+
throw new Error('Termshark handoff capture does not match the evidence bundle identity');
|
|
48
|
+
}
|
|
49
|
+
const displayFilterDigest = `sha256:${sha256(options.displayFilter)}`;
|
|
50
|
+
if (context.display_filter_digest !== displayFilterDigest) {
|
|
51
|
+
throw new Error('Termshark handoff display filter does not match the evidence analysis context');
|
|
52
|
+
}
|
|
53
|
+
const canonicalConfigRoot = await validateDirectory(options.configRoot, 'Termshark config root');
|
|
54
|
+
const profile = await validateProfile(options.profile, canonicalConfigRoot);
|
|
55
|
+
const focus = normalizeFocus(options.focus);
|
|
56
|
+
validateFocusEvidence(options.evidence, options.contextDigest, focus);
|
|
57
|
+
const effectiveFilter = focusedDisplayFilter(options.displayFilter, focus);
|
|
58
|
+
const base = baseHandoff(options, profile, canonicalConfigRoot, focus, displayFilterDigest, effectiveFilter);
|
|
59
|
+
if (options.termshark.status === 'missing' || !options.termshark.path || !options.termshark.version) {
|
|
60
|
+
return {
|
|
61
|
+
...base,
|
|
62
|
+
status: 'unavailable',
|
|
63
|
+
diagnostics: [...options.termshark.diagnostics, 'Termshark is optional; TShark evidence remains valid and available.'],
|
|
64
|
+
command: null,
|
|
65
|
+
commandPreview: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (options.termshark.status !== 'supported' || !semver.valid(options.termshark.version)
|
|
69
|
+
|| !semver.satisfies(options.termshark.version, '>=2.4.0 <3.0.0')) {
|
|
70
|
+
return {
|
|
71
|
+
...base,
|
|
72
|
+
status: 'incompatible',
|
|
73
|
+
diagnostics: [...options.termshark.diagnostics, `Termshark ${options.termshark.version} is outside the supported >=2.4.0 <3.0.0 range; TShark evidence remains valid.`],
|
|
74
|
+
command: null,
|
|
75
|
+
commandPreview: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const command = safeProcessSpec(options.termshark.path, [
|
|
79
|
+
'-r', options.capturePath,
|
|
80
|
+
'-Y', effectiveFilter,
|
|
81
|
+
'-C', profile.name,
|
|
82
|
+
]);
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
status: 'ready',
|
|
86
|
+
diagnostics: [...options.termshark.diagnostics],
|
|
87
|
+
command,
|
|
88
|
+
commandPreview: formatCommandPreview(command),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function launchTermsharkHandoff(handoff, action, host = createDefaultTermsharkLaunchHost()) {
|
|
92
|
+
if (action.explicitOperatorAction !== true)
|
|
93
|
+
throw new Error('Termshark launch requires an explicit operator action');
|
|
94
|
+
if (handoff.status !== 'ready' || !handoff.command)
|
|
95
|
+
throw new Error('Termshark handoff is not ready to launch');
|
|
96
|
+
await validateLaunchIdentity(handoff);
|
|
97
|
+
const actual = await hashEvidenceFile(handoff.capturePath, 'source-capture');
|
|
98
|
+
if (`sha256:${actual.value}` !== handoff.captureDigest) {
|
|
99
|
+
throw new Error('Termshark launch capture no longer matches the reviewed handoff');
|
|
100
|
+
}
|
|
101
|
+
return host.run(handoff.command, { configRoot: handoff.configRoot, tsharkPath: handoff.tsharkPath, signal: action.signal });
|
|
102
|
+
}
|
|
103
|
+
export function recordTermsharkReview(handoff, evidence, input) {
|
|
104
|
+
if (evidence.bundle_id !== handoff.evidenceBundleId || evidence.capture.capture_digest !== handoff.captureDigest) {
|
|
105
|
+
throw new Error('Operator note evidence does not match the reviewed Termshark handoff');
|
|
106
|
+
}
|
|
107
|
+
if (!input.analyst.trim() || !input.note.trim() || input.citations.length === 0)
|
|
108
|
+
throw new Error('Analyst, note, and at least one packet locator are required');
|
|
109
|
+
const evidenceCitations = new Set(evidence.evidence_items.flatMap(item => item.citations.map(formatEvidenceCitation)));
|
|
110
|
+
for (const citation of input.citations) {
|
|
111
|
+
if (citation.capture_digest !== handoff.captureDigest || !evidenceCitations.has(formatEvidenceCitation(citation))) {
|
|
112
|
+
throw new Error('Operator note citation is not present in the evidence bundle');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
analyst: input.analyst,
|
|
117
|
+
recordedAt: input.recordedAt ?? new Date().toISOString(),
|
|
118
|
+
note: input.note,
|
|
119
|
+
citations: structuredClone(input.citations),
|
|
120
|
+
source: 'operator-returned-note',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function baseHandoff(options, profile, configRoot, focus, displayFilterDigest, effectiveFilter) {
|
|
124
|
+
const fingerprint = sha256(JSON.stringify({
|
|
125
|
+
bundle: options.evidence.bundle_id,
|
|
126
|
+
capture: options.evidence.capture.capture_digest,
|
|
127
|
+
context: options.contextDigest,
|
|
128
|
+
filter: effectiveFilter,
|
|
129
|
+
profile,
|
|
130
|
+
tool: options.termshark,
|
|
131
|
+
}));
|
|
132
|
+
return {
|
|
133
|
+
schema: TERMSHARK_HANDOFF_SCHEMA,
|
|
134
|
+
handoffId: `termshark-handoff:${fingerprint.slice(0, 24)}`,
|
|
135
|
+
evidenceBundleId: options.evidence.bundle_id,
|
|
136
|
+
capturePath: options.capturePath,
|
|
137
|
+
captureDigest: String(options.evidence.capture.capture_digest),
|
|
138
|
+
analysisContextDigest: options.contextDigest,
|
|
139
|
+
displayFilterDigest,
|
|
140
|
+
effectiveFilterDigest: `sha256:${sha256(effectiveFilter)}`,
|
|
141
|
+
focus,
|
|
142
|
+
tool: { path: options.termshark.path, version: options.termshark.version },
|
|
143
|
+
profile,
|
|
144
|
+
configRoot,
|
|
145
|
+
tsharkPath: options.tsharkPath,
|
|
146
|
+
tsharkVersion: String(findAnalysisContext(options.evidence, options.contextDigest).tool.version),
|
|
147
|
+
createdAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
148
|
+
completionClaim: 'operator-returned-notes-only',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function findAnalysisContext(evidence, digest) {
|
|
152
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
153
|
+
throw new Error('Termshark handoff requires a canonical analysis context digest');
|
|
154
|
+
const contexts = evidence.capture.analysis_contexts;
|
|
155
|
+
const context = contexts.find(candidate => candidate.context_digest === digest);
|
|
156
|
+
if (!context)
|
|
157
|
+
throw new Error('Termshark handoff analysis context is absent from the evidence bundle');
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
160
|
+
async function validateDirectory(directory, label) {
|
|
161
|
+
if (!path.isAbsolute(directory))
|
|
162
|
+
throw new Error(`${label} must be an explicit absolute path`);
|
|
163
|
+
const stat = await lstat(directory);
|
|
164
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
165
|
+
throw new Error(`${label} must be a real non-symlink directory`);
|
|
166
|
+
return realpath(directory);
|
|
167
|
+
}
|
|
168
|
+
async function validateProfile(profile, configRoot) {
|
|
169
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profile.name))
|
|
170
|
+
throw new Error('Termshark profile name is invalid');
|
|
171
|
+
if (!profile.path)
|
|
172
|
+
return { name: profile.name };
|
|
173
|
+
if (!path.isAbsolute(profile.path))
|
|
174
|
+
throw new Error('Termshark profile path must be explicit and absolute');
|
|
175
|
+
const canonical = await realpath(profile.path);
|
|
176
|
+
const relative = path.relative(configRoot, canonical);
|
|
177
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
178
|
+
throw new Error('Termshark profile path must remain below the config root');
|
|
179
|
+
}
|
|
180
|
+
const identity = await hashEvidenceFile(canonical, 'derived-artifact');
|
|
181
|
+
return { name: profile.name, path: canonical, digest: `sha256:${identity.value}` };
|
|
182
|
+
}
|
|
183
|
+
function normalizeFocus(focus) {
|
|
184
|
+
if (!focus)
|
|
185
|
+
return {};
|
|
186
|
+
if (focus.frame !== undefined && (!Number.isSafeInteger(focus.frame) || focus.frame < 1))
|
|
187
|
+
throw new Error('Termshark frame focus is invalid');
|
|
188
|
+
if (focus.stream && (!['tcp', 'udp'].includes(focus.stream.protocol) || !Number.isSafeInteger(focus.stream.id) || focus.stream.id < 0)) {
|
|
189
|
+
throw new Error('Termshark stream focus is invalid');
|
|
190
|
+
}
|
|
191
|
+
return structuredClone(focus);
|
|
192
|
+
}
|
|
193
|
+
function validateFocusEvidence(evidence, contextDigest, focus) {
|
|
194
|
+
if (focus.frame === undefined && focus.stream === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const citations = evidence.evidence_items.flatMap(item => (item.citations ?? []));
|
|
197
|
+
if (focus.frame !== undefined && !citations.some(citation => citation.locator.type === 'frame'
|
|
198
|
+
&& citation.locator.frame_number === focus.frame)) {
|
|
199
|
+
throw new Error('Termshark frame focus is absent from the evidence bundle');
|
|
200
|
+
}
|
|
201
|
+
if (focus.stream && !citations.some(citation => citation.locator.type === 'stream'
|
|
202
|
+
&& citation.locator.protocol === focus.stream?.protocol
|
|
203
|
+
&& citation.locator.stream_id === focus.stream.id
|
|
204
|
+
&& citation.locator.context_digest === contextDigest)) {
|
|
205
|
+
throw new Error('Termshark stream focus is absent from the selected analysis context');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function validateLaunchIdentity(handoff) {
|
|
209
|
+
if (!handoff.command || handoff.command.shell !== false || handoff.command.file !== handoff.tool.path) {
|
|
210
|
+
throw new Error('Termshark launch command no longer matches the reviewed tool');
|
|
211
|
+
}
|
|
212
|
+
const args = handoff.command.args;
|
|
213
|
+
if (args.length !== 6 || args[0] !== '-r' || args[1] !== handoff.capturePath
|
|
214
|
+
|| args[2] !== '-Y' || args[4] !== '-C' || args[5] !== handoff.profile.name
|
|
215
|
+
|| `sha256:${sha256(args[3])}` !== handoff.effectiveFilterDigest) {
|
|
216
|
+
throw new Error('Termshark launch arguments no longer match the reviewed handoff');
|
|
217
|
+
}
|
|
218
|
+
const configRoot = await validateDirectory(handoff.configRoot, 'Termshark config root');
|
|
219
|
+
if (configRoot !== handoff.configRoot)
|
|
220
|
+
throw new Error('Termshark config root identity changed after review');
|
|
221
|
+
if (handoff.profile.path) {
|
|
222
|
+
const profile = await validateProfile({ name: handoff.profile.name, path: handoff.profile.path }, configRoot);
|
|
223
|
+
if (profile.path !== handoff.profile.path || profile.digest !== handoff.profile.digest) {
|
|
224
|
+
throw new Error('Termshark profile identity changed after review');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function focusedDisplayFilter(displayFilter, focus) {
|
|
229
|
+
if (!displayFilter.trim() || displayFilter.length > 8192 || displayFilter.includes('\0'))
|
|
230
|
+
throw new Error('Termshark display filter is invalid');
|
|
231
|
+
const additions = [];
|
|
232
|
+
if (focus.frame !== undefined)
|
|
233
|
+
additions.push(`frame.number == ${focus.frame}`);
|
|
234
|
+
if (focus.stream)
|
|
235
|
+
additions.push(`${focus.stream.protocol}.stream == ${focus.stream.id}`);
|
|
236
|
+
return [displayFilter, ...additions].map(expression => `(${expression})`).join(' && ');
|
|
237
|
+
}
|
|
238
|
+
function validateHandoffOptions(options) {
|
|
239
|
+
if (!path.isAbsolute(options.capturePath))
|
|
240
|
+
throw new Error('Termshark capture path must be explicit and absolute');
|
|
241
|
+
if (!path.isAbsolute(options.tsharkPath))
|
|
242
|
+
throw new Error('TShark path must be explicit and absolute');
|
|
243
|
+
if (options.termshark.path !== null && !path.isAbsolute(options.termshark.path))
|
|
244
|
+
throw new Error('Termshark path must be explicit and absolute');
|
|
245
|
+
}
|
|
246
|
+
function formatCommandPreview(spec) {
|
|
247
|
+
return [spec.file, ...spec.args].map(argument => `'${argument.replace(/'/g, `'"'"'`)}'`).join(' ');
|
|
248
|
+
}
|
|
249
|
+
function sha256(value) {
|
|
250
|
+
return createHash('sha256').update(value).digest('hex');
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=termshark.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export const PACKET_VERIFICATION_SCHEMA = 'aiwg.sdlc.packet-verification.v1';
|
|
2
|
+
const METRICS = [
|
|
3
|
+
'summary.packet_count',
|
|
4
|
+
'summary.endpoint_count',
|
|
5
|
+
'summary.conversation_count',
|
|
6
|
+
'summary.duration_seconds',
|
|
7
|
+
'summary.tcp_retransmission_count',
|
|
8
|
+
'summary.tcp_reset_count',
|
|
9
|
+
'summary.encrypted_transport_frame_count',
|
|
10
|
+
];
|
|
11
|
+
export function comparePacketEvidenceForVerification(input) {
|
|
12
|
+
validateInput(input);
|
|
13
|
+
const baselineRecipe = recipeId(input.baseline.evidence);
|
|
14
|
+
const candidateRecipe = recipeId(input.candidate.evidence);
|
|
15
|
+
const baselineContext = analysisContext(input.baseline.evidence);
|
|
16
|
+
const candidateContext = analysisContext(input.candidate.evidence);
|
|
17
|
+
const hardReasons = [];
|
|
18
|
+
if (baselineRecipe !== candidateRecipe)
|
|
19
|
+
hardReasons.push(`Recipe mismatch: ${baselineRecipe} vs ${candidateRecipe}.`);
|
|
20
|
+
if (baselineContext.display_filter_digest !== candidateContext.display_filter_digest)
|
|
21
|
+
hardReasons.push('Display-filter digests differ.');
|
|
22
|
+
if (baselineContext.tool?.version !== candidateContext.tool?.version)
|
|
23
|
+
hardReasons.push('TShark versions differ.');
|
|
24
|
+
if (input.baseline.topologyFingerprint !== input.candidate.topologyFingerprint)
|
|
25
|
+
hardReasons.push('Environment topology fingerprints differ.');
|
|
26
|
+
if (!['completed', 'empty'].includes(input.baseline.evidence.status) || !['completed', 'empty'].includes(input.candidate.evidence.status)) {
|
|
27
|
+
hardReasons.push('At least one evidence bundle is partial or unusable.');
|
|
28
|
+
}
|
|
29
|
+
const excludedMetrics = [];
|
|
30
|
+
const clockReasons = [];
|
|
31
|
+
if (input.baseline.clockOffsetMs === null || input.candidate.clockOffsetMs === null) {
|
|
32
|
+
excludedMetrics.push('summary.duration_seconds');
|
|
33
|
+
clockReasons.push('Clock alignment is unknown; timing comparison is excluded.');
|
|
34
|
+
}
|
|
35
|
+
else if (Math.abs(input.baseline.clockOffsetMs - input.candidate.clockOffsetMs) > input.conditions.clockAlignment.toleranceMs) {
|
|
36
|
+
excludedMetrics.push('summary.duration_seconds');
|
|
37
|
+
clockReasons.push('Clock-offset difference exceeds the declared tolerance; timing comparison is excluded.');
|
|
38
|
+
}
|
|
39
|
+
const comparableMetrics = hardReasons.length > 0 ? [] : METRICS.filter(metric => !excludedMetrics.includes(metric));
|
|
40
|
+
const baselineMetrics = summaryMetrics(input.baseline.evidence);
|
|
41
|
+
const candidateMetrics = summaryMetrics(input.candidate.evidence);
|
|
42
|
+
const metricDifferences = comparableMetrics.flatMap(metric => {
|
|
43
|
+
const baseline = baselineMetrics.get(metric);
|
|
44
|
+
const candidate = candidateMetrics.get(metric);
|
|
45
|
+
return typeof baseline !== 'number' || typeof candidate !== 'number'
|
|
46
|
+
? []
|
|
47
|
+
: [{ metric, baseline, candidate, delta: candidate - baseline }];
|
|
48
|
+
});
|
|
49
|
+
const protocolDifferences = hardReasons.length > 0 ? [] : compareProtocols(baselineMetrics, candidateMetrics);
|
|
50
|
+
const reasons = [...hardReasons, ...clockReasons];
|
|
51
|
+
return {
|
|
52
|
+
schema: PACKET_VERIFICATION_SCHEMA,
|
|
53
|
+
verificationId: input.verificationId,
|
|
54
|
+
useCase: input.useCase,
|
|
55
|
+
traceability: {
|
|
56
|
+
requirementIds: [...input.requirementIds],
|
|
57
|
+
testIds: [...input.testIds],
|
|
58
|
+
defectOrChangeId: input.defectOrChangeId,
|
|
59
|
+
baseline: traceSide(input.baseline, baselineRecipe),
|
|
60
|
+
candidate: traceSide(input.candidate, candidateRecipe),
|
|
61
|
+
},
|
|
62
|
+
comparability: {
|
|
63
|
+
status: hardReasons.length > 0 ? 'incomparable' : excludedMetrics.length > 0 ? 'partially-comparable' : 'comparable',
|
|
64
|
+
reasons,
|
|
65
|
+
excludedMetrics,
|
|
66
|
+
},
|
|
67
|
+
conditions: {
|
|
68
|
+
...structuredClone(input.conditions),
|
|
69
|
+
environments: { baseline: input.baseline.environmentId, candidate: input.candidate.environmentId },
|
|
70
|
+
topologyFingerprints: { baseline: input.baseline.topologyFingerprint, candidate: input.candidate.topologyFingerprint },
|
|
71
|
+
clockOffsetsMs: { baseline: input.baseline.clockOffsetMs, candidate: input.candidate.clockOffsetMs },
|
|
72
|
+
},
|
|
73
|
+
normalization: {
|
|
74
|
+
comparedFields: [...comparableMetrics, 'summary.protocol_hierarchy'],
|
|
75
|
+
excludedVolatileFields: ['capture digest', 'frame number', 'stream ID', 'absolute timestamp', 'endpoint identity', 'ephemeral port'],
|
|
76
|
+
},
|
|
77
|
+
metricDifferences,
|
|
78
|
+
protocolDifferences,
|
|
79
|
+
attachments: [attachment('baseline', input.baseline, baselineRecipe), attachment('candidate', input.candidate, candidateRecipe)],
|
|
80
|
+
performanceInterpretation: {
|
|
81
|
+
causality: 'not-established',
|
|
82
|
+
statement: 'Differences are observations under the recorded traffic, load, topology, vantage-point, sample-window, and clock conditions; they do not establish that the build or change caused the result.',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function validateInput(input) {
|
|
87
|
+
for (const [label, value] of Object.entries({ verification: input.verificationId, change: input.defectOrChangeId }))
|
|
88
|
+
required(value, label);
|
|
89
|
+
if (input.requirementIds.length === 0 || input.testIds.length === 0)
|
|
90
|
+
throw new Error('Packet verification requires requirement and test identities');
|
|
91
|
+
stringArray(input.requirementIds, 'Requirement IDs');
|
|
92
|
+
stringArray(input.testIds, 'Test IDs');
|
|
93
|
+
stringArray(input.conditions.topologyAssumptions, 'Topology assumptions');
|
|
94
|
+
if (input.conditions.topologyAssumptions.length === 0)
|
|
95
|
+
throw new Error('Packet verification requires topology assumptions');
|
|
96
|
+
for (const value of [input.baseline.buildId, input.candidate.buildId, input.baseline.environmentId, input.candidate.environmentId,
|
|
97
|
+
input.baseline.topologyFingerprint, input.candidate.topologyFingerprint, input.conditions.vantagePoint, input.conditions.traffic.scenario,
|
|
98
|
+
input.conditions.traffic.isolation, input.conditions.load, input.conditions.clockAlignment.method])
|
|
99
|
+
required(value, 'Verification condition');
|
|
100
|
+
if (!Number.isFinite(input.conditions.sampleWindowSeconds) || input.conditions.sampleWindowSeconds <= 0
|
|
101
|
+
|| !Number.isFinite(input.conditions.clockAlignment.toleranceMs) || input.conditions.clockAlignment.toleranceMs < 0) {
|
|
102
|
+
throw new Error('Packet verification measurement bounds are invalid');
|
|
103
|
+
}
|
|
104
|
+
if (input.conditions.traffic.capturesUnrelatedTraffic !== false)
|
|
105
|
+
throw new Error('Packet verification must exclude unrelated traffic');
|
|
106
|
+
if (input.executionContext === 'ci' && input.conditions.traffic.kind !== 'synthetic') {
|
|
107
|
+
throw new Error('CI packet verification requires isolated synthetic traffic');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function analysisContext(evidence) {
|
|
111
|
+
const contexts = evidence.capture.analysis_contexts;
|
|
112
|
+
if (!Array.isArray(contexts) || contexts.length !== 1)
|
|
113
|
+
throw new Error('Packet verification requires one analysis context per bundle');
|
|
114
|
+
return contexts[0];
|
|
115
|
+
}
|
|
116
|
+
function recipeId(evidence) {
|
|
117
|
+
return required(evidence.provenance?.recipe_id, 'Recipe identity');
|
|
118
|
+
}
|
|
119
|
+
function traceSide(side, recipe) {
|
|
120
|
+
return { buildId: side.buildId, bundleId: side.evidence.bundle_id, captureDigest: String(side.evidence.capture.capture_digest), recipeId: recipe };
|
|
121
|
+
}
|
|
122
|
+
function attachment(sideName, side, recipe) {
|
|
123
|
+
return {
|
|
124
|
+
side: sideName,
|
|
125
|
+
bundleId: side.evidence.bundle_id,
|
|
126
|
+
captureDigest: String(side.evidence.capture.capture_digest),
|
|
127
|
+
recipeId: recipe,
|
|
128
|
+
artifactDigests: side.evidence.artifacts.map((artifact) => `${artifact.digest.algorithm}:${artifact.digest.value}`),
|
|
129
|
+
rawCaptureIncluded: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function summaryMetrics(evidence) {
|
|
133
|
+
const summary = evidence.evidence_items.find(item => String(item.evidence_id).includes('capture-summary'));
|
|
134
|
+
const fields = Array.isArray(summary?.observed_fields) ? summary.observed_fields : [];
|
|
135
|
+
const result = new Map();
|
|
136
|
+
for (const field of fields) {
|
|
137
|
+
if (typeof field.value === 'number' || typeof field.value === 'string')
|
|
138
|
+
result.set(field.name, field.value);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function compareProtocols(baseline, candidate) {
|
|
143
|
+
const left = protocolCounts(baseline.get('summary.protocol_hierarchy'));
|
|
144
|
+
const right = protocolCounts(candidate.get('summary.protocol_hierarchy'));
|
|
145
|
+
return [...new Set([...left.keys(), ...right.keys()])].sort().map(protocol => ({
|
|
146
|
+
protocol, baseline: left.get(protocol) ?? 0, candidate: right.get(protocol) ?? 0,
|
|
147
|
+
delta: (right.get(protocol) ?? 0) - (left.get(protocol) ?? 0),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
function protocolCounts(value) {
|
|
151
|
+
const result = new Map();
|
|
152
|
+
if (typeof value !== 'string')
|
|
153
|
+
return result;
|
|
154
|
+
for (const entry of value.split(',')) {
|
|
155
|
+
const [protocol, count] = entry.split(':');
|
|
156
|
+
if (protocol && Number.isFinite(Number(count)))
|
|
157
|
+
result.set(protocol, Number(count));
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function required(value, label) {
|
|
162
|
+
if (typeof value !== 'string' || !value.trim() || value.includes('\0'))
|
|
163
|
+
throw new Error(`${label} is required`);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function stringArray(value, label) {
|
|
167
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item.trim()))
|
|
168
|
+
throw new Error(`${label} must contain non-empty strings`);
|
|
169
|
+
return [...value];
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=verification.js.map
|
|
@@ -5,6 +5,9 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
6
|
import { parse, stringify } from 'yaml';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { getWritingChannelPack } from '../writing/channel-packs.js';
|
|
9
|
+
import { WriterProfileStore } from '../writing/writer-profile-store.js';
|
|
10
|
+
import { compileWriterProfile } from '../writing/writer-profile.js';
|
|
8
11
|
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
9
12
|
const MODE_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*$/;
|
|
10
13
|
const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers', 'machine-readable-blocks'];
|
|
@@ -41,6 +44,12 @@ const outputModeProfileSchema = z.object({
|
|
|
41
44
|
}
|
|
42
45
|
});
|
|
43
46
|
const BUILTINS = [
|
|
47
|
+
...['article', 'social', 'email', 'engineering', 'conversation'].map(channel => ({
|
|
48
|
+
id: `channel-${channel}`, version: '1.0.0', description: `Advisory ${channel} structure pack`,
|
|
49
|
+
kind: 'structure', stage: 'structure', order: 300,
|
|
50
|
+
instructions: getWritingChannelPack(channel).instructions, provenance: { source: 'AIWG channel packs', license: 'MIT' },
|
|
51
|
+
validation: { level: 'advisory' }, protectedContent: [...PROTECTED],
|
|
52
|
+
})),
|
|
44
53
|
{
|
|
45
54
|
id: 'unaltered', version: '1.0.0', description: 'No-op mode; preserves the provider output path unchanged.',
|
|
46
55
|
kind: 'presentation', stage: 'presentation', order: -1000, instructions: '',
|
|
@@ -126,7 +135,18 @@ async function loadVoiceAdapters(frameworkRoot) {
|
|
|
126
135
|
}
|
|
127
136
|
return result;
|
|
128
137
|
}
|
|
129
|
-
|
|
138
|
+
async function loadWriterAdapters(cwd, scope, context) {
|
|
139
|
+
const store = new WriterProfileStore({ cwd, scope });
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const id of await store.list()) {
|
|
142
|
+
const compiled = compileWriterProfile(await store.read(id), context);
|
|
143
|
+
result.push({ ...validateOutputModeProfile(compiled.profile), source: scope, sourcePath: join(store.directory, `${id}.json`) });
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
export async function loadOutputModeRegistry(cwd, frameworkRoot, context = {}) {
|
|
148
|
+
if (context.task !== undefined && (typeof context.task !== 'string' || !context.task.trim() || context.task.length > 120))
|
|
149
|
+
throw new Error('Invalid writer task context');
|
|
130
150
|
const registry = new Map();
|
|
131
151
|
for (const profile of BUILTINS)
|
|
132
152
|
registry.set(profile.id, { ...profile, source: 'builtin' });
|
|
@@ -134,9 +154,16 @@ export async function loadOutputModeRegistry(cwd, frameworkRoot) {
|
|
|
134
154
|
if (!registry.has(profile.id))
|
|
135
155
|
registry.set(profile.id, profile);
|
|
136
156
|
// User overrides built-ins; project overrides user.
|
|
137
|
-
for (const entry of [...profileDirs(cwd)].reverse())
|
|
138
|
-
|
|
157
|
+
for (const entry of [...profileDirs(cwd)].reverse()) {
|
|
158
|
+
const profiles = await loadDirectory(entry.dir, entry.source);
|
|
159
|
+
const writers = await loadWriterAdapters(cwd, entry.source, context);
|
|
160
|
+
const ids = new Set(profiles.map(profile => profile.id));
|
|
161
|
+
for (const writer of writers)
|
|
162
|
+
if (ids.has(writer.id))
|
|
163
|
+
throw new Error(`Duplicate output mode '${writer.id}' from a writer sidecar and a mode file in the same scope.`);
|
|
164
|
+
for (const profile of [...profiles, ...writers])
|
|
139
165
|
registry.set(profile.id, profile);
|
|
166
|
+
}
|
|
140
167
|
return registry;
|
|
141
168
|
}
|
|
142
169
|
function statePath(cwd, scope) {
|
|
@@ -162,8 +189,8 @@ export async function writeOutputModeState(cwd, scope, modes) {
|
|
|
162
189
|
await writeFile(path, stringify({ version: 1, modes }), 'utf8');
|
|
163
190
|
return path;
|
|
164
191
|
}
|
|
165
|
-
export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], overrides = {}) {
|
|
166
|
-
const registry = await loadOutputModeRegistry(cwd, frameworkRoot);
|
|
192
|
+
export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], overrides = {}, context = {}) {
|
|
193
|
+
const registry = await loadOutputModeRegistry(cwd, frameworkRoot, context);
|
|
167
194
|
const project = overrides.project ?? (await readOutputModeState(cwd, 'project')).modes;
|
|
168
195
|
const session = overrides.session ?? (await readOutputModeState(cwd, 'session')).modes;
|
|
169
196
|
for (const [scope, modes] of [['project', project], ['session', session], ['invocation', invocation]]) {
|
|
@@ -188,7 +215,11 @@ export async function resolveOutputModes(cwd, frameworkRoot, invocation = [], ov
|
|
|
188
215
|
throw new Error(`Output modes '${a.id}' and '${b.id}' conflict. Disable one or configure an explicit merge strategy.`);
|
|
189
216
|
if (a.kind === b.kind && a.kind !== 'voice')
|
|
190
217
|
throw new Error(`Output modes '${a.id}' and '${b.id}' share kind '${a.kind}' without a merge strategy.`);
|
|
191
|
-
|
|
218
|
+
const writerSelected = a.id.startsWith('writer-') || b.id.startsWith('writer-');
|
|
219
|
+
const missingMerge = writerSelected
|
|
220
|
+
? a.mergeStrategy !== 'weighted-voice' || b.mergeStrategy !== 'weighted-voice'
|
|
221
|
+
: a.mergeStrategy !== 'weighted-voice' && b.mergeStrategy !== 'weighted-voice';
|
|
222
|
+
if (a.kind === 'voice' && b.kind === 'voice' && missingMerge)
|
|
192
223
|
throw new Error(`Voice modes '${a.id}' and '${b.id}' require an explicit weighted-voice merge strategy.`);
|
|
193
224
|
}
|
|
194
225
|
for (const mode of modes)
|