@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.
Files changed (95) hide show
  1. package/README.md +21 -0
  2. package/THIRD_PARTY_NOTICES.md +16 -0
  3. package/agentic/code/providers/capability-matrix.yaml +45 -3
  4. package/agentic/code/providers/deepseek-harness/README.md +8 -0
  5. package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
  6. package/agentic/code/providers/model-capabilities.v1.json +11 -0
  7. package/agentic/code/providers/model-catalog.v1.json +8 -0
  8. package/bin/aiwg.mjs +1 -0
  9. package/dist/src/api/index.d.ts +14 -0
  10. package/dist/src/api/index.js +14 -0
  11. package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
  12. package/dist/src/artifacts/index-files.js +17 -2
  13. package/dist/src/artifacts/repair.js +55 -6
  14. package/dist/src/catalog/cli.js +21 -7
  15. package/dist/src/catalog/cli.mjs +22 -7
  16. package/dist/src/cli/agent-spawn.js +10 -1
  17. package/dist/src/cli/handlers/artifacts.js +22 -3
  18. package/dist/src/cli/handlers/help.js +3 -0
  19. package/dist/src/cli/handlers/index.js +5 -1
  20. package/dist/src/cli/handlers/models.js +2 -2
  21. package/dist/src/cli/handlers/output-mode.js +1 -1
  22. package/dist/src/cli/handlers/runtime-info.js +1 -1
  23. package/dist/src/cli/handlers/sessions.js +55 -15
  24. package/dist/src/cli/handlers/steward.js +1 -1
  25. package/dist/src/cli/handlers/subcommands.js +5 -0
  26. package/dist/src/cli/handlers/use.js +37 -0
  27. package/dist/src/cli/handlers/writer-profile.js +110 -0
  28. package/dist/src/cli/handlers/writing.js +122 -0
  29. package/dist/src/cli/router.js +5 -1
  30. package/dist/src/config/project-artifacts-runtime.mjs +33 -1
  31. package/dist/src/config/project-artifacts.js +1 -1
  32. package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
  33. package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
  34. package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
  35. package/dist/src/dataset/fortemi-live-qualification.js +20 -21
  36. package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
  37. package/dist/src/dataset/fortemi-run-receipt.js +74 -0
  38. package/dist/src/dataset/index.d.ts +2 -0
  39. package/dist/src/dataset/index.js +2 -0
  40. package/dist/src/extensions/commands/definitions.js +24 -0
  41. package/dist/src/extensions/manifest.js +3 -0
  42. package/dist/src/mcp/server.mjs +2 -0
  43. package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
  44. package/dist/src/models/model-capabilities.v1.json +11 -0
  45. package/dist/src/models/model-catalog.v1.json +8 -0
  46. package/dist/src/models/provider-policy.js +1 -1
  47. package/dist/src/network-analysis/analyzer.js +667 -0
  48. package/dist/src/network-analysis/citations.js +107 -0
  49. package/dist/src/network-analysis/forensics.js +132 -0
  50. package/dist/src/network-analysis/governance.js +216 -0
  51. package/dist/src/network-analysis/index.js +10 -0
  52. package/dist/src/network-analysis/probe.js +405 -0
  53. package/dist/src/network-analysis/recipes.js +88 -0
  54. package/dist/src/network-analysis/research.js +181 -0
  55. package/dist/src/network-analysis/termshark.js +252 -0
  56. package/dist/src/network-analysis/verification.js +171 -0
  57. package/dist/src/output-modes/registry.js +37 -6
  58. package/dist/src/output-modes/runtime.js +164 -28
  59. package/dist/src/providers/capability-matrix.yaml +45 -3
  60. package/dist/src/providers/provider-definitions.js +49 -0
  61. package/dist/src/providers/provider-inventory.js +1 -0
  62. package/dist/src/providers/transformation-receipt.js +3 -2
  63. package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
  64. package/dist/src/sessions/batch-import.js +7 -0
  65. package/dist/src/sessions/contracts.js +2 -1
  66. package/dist/src/sessions/index.js +1 -0
  67. package/dist/src/sessions/workspace-discovery.js +5 -1
  68. package/dist/src/skills/deployer.js +6 -6
  69. package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
  70. package/dist/src/writing/channel-packs.js +13 -0
  71. package/dist/src/writing/contextual-diagnostics.js +142 -0
  72. package/dist/src/writing/example-generator.js +7 -6
  73. package/dist/src/writing/exemplar-selection.js +186 -0
  74. package/dist/src/writing/fidelity.js +61 -0
  75. package/dist/src/writing/validation-engine.js +32 -15
  76. package/dist/src/writing/voice-evaluation.js +301 -0
  77. package/dist/src/writing/voice-revision.js +201 -0
  78. package/dist/src/writing/writer-migration.js +216 -0
  79. package/dist/src/writing/writer-profile-legacy.js +145 -0
  80. package/dist/src/writing/writer-profile-store.js +117 -0
  81. package/dist/src/writing/writer-profile.js +222 -0
  82. package/dist/src/writing/writing-brief.js +166 -0
  83. package/dist/src/writing/writing-channels.js +63 -0
  84. package/dist/src/writing/writing-consumer.js +39 -0
  85. package/dist/src/writing/writing-receipt.js +266 -0
  86. package/package.json +1 -1
  87. package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
  88. package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
  89. package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
  90. package/tools/agents/deploy-agents.mjs +7 -3
  91. package/tools/agents/providers/antigravity.mjs +1 -1
  92. package/tools/agents/providers/base.mjs +3 -2
  93. package/tools/agents/providers/deepseek-harness.mjs +66 -0
  94. package/tools/agents/providers/hermes.mjs +1 -1
  95. package/tools/agents/providers/openhuman.mjs +2 -2
@@ -0,0 +1,201 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { assessWritingFidelity } from './fidelity.js';
3
+ import { parseWritingBrief } from './writing-brief.js';
4
+ import { parseWriterProfile } from './writer-profile.js';
5
+ const hash = (text) => createHash('sha256').update(text).digest('hex');
6
+ function validateEdits(source, edits) {
7
+ const sorted = structuredClone(edits).sort((a, b) => a.start - b.start || a.end - b.end);
8
+ const boundary = (at) => Number.isSafeInteger(at) && at >= 0 && at <= source.length && !(at > 0 && at < source.length && /[\uD800-\uDBFF]/.test(source[at - 1]) && /[\uDC00-\uDFFF]/.test(source[at]));
9
+ if (new Set(sorted.map(e => e.id)).size !== sorted.length)
10
+ throw new Error('Duplicate revision edit IDs');
11
+ for (let i = 0; i < sorted.length; i++) {
12
+ const e = sorted[i];
13
+ if (typeof e.id !== 'string' || !e.id || !boundary(e.start) || !boundary(e.end) || e.start > e.end || typeof e.expected !== 'string' || source.slice(e.start, e.end) !== e.expected || typeof e.replacement !== 'string' || !e.reason?.trim())
14
+ throw new Error('Invalid or stale revision edit');
15
+ if (i && (e.start < sorted[i - 1].end || e.start === sorted[i - 1].start))
16
+ throw new Error('Overlapping revision edits require review');
17
+ }
18
+ return sorted;
19
+ }
20
+ function applyEdits(source, edits) {
21
+ let output = source;
22
+ for (const e of [...validateEdits(source, edits)].reverse())
23
+ output = output.slice(0, e.start) + e.replacement + output.slice(e.end);
24
+ return output;
25
+ }
26
+ export function createRevisionReview(original, edits, origin = 'human') {
27
+ if (origin !== 'human' && origin !== 'generated')
28
+ throw new Error('Invalid correction origin');
29
+ return { original, sourceHash: hash(original), edits: validateEdits(original, edits), candidate: applyEdits(original, edits), origin };
30
+ }
31
+ export function acceptRevisionEdits(review, decision) {
32
+ const frozen = createRevisionReview(review.original, review.edits, review.origin);
33
+ if (decision.sourceHash !== frozen.sourceHash || review.sourceHash !== frozen.sourceHash || review.candidate !== frozen.candidate)
34
+ throw new Error('Stale revision source or candidate');
35
+ const ids = [...decision.acceptedIds, ...decision.rejectedIds];
36
+ if (!decision.actor.trim() || new Set(ids).size !== ids.length || ids.some(id => !frozen.edits.some(e => e.id === id)) || ids.length !== frozen.edits.length)
37
+ throw new Error('Every edit requires exactly one explicit human decision');
38
+ const output = applyEdits(frozen.original, frozen.edits.filter(e => decision.acceptedIds.includes(e.id)));
39
+ return { ...frozen, actor: decision.actor, acceptedIds: [...decision.acceptedIds], rejectedIds: [...decision.rejectedIds], output, outputHash: hash(output), approval: 'explicit-human' };
40
+ }
41
+ export function undoRevisionReview(review) {
42
+ if (hash(review.original) !== review.sourceHash)
43
+ throw new Error('Stale revision source');
44
+ return review.original;
45
+ }
46
+ class RevisionStop extends Error {
47
+ reason;
48
+ constructor(reason) {
49
+ super(reason);
50
+ this.reason = reason;
51
+ }
52
+ }
53
+ /** Bounded automatic assistance; artifacts survive cancellation and no judge is required for human review. */
54
+ export async function runVoiceRevision(original, input) {
55
+ const options = { ...input, ...(input.brief ? { brief: parseWritingBrief(input.brief) } : {}) };
56
+ const maxPasses = options.maxPasses ?? 2;
57
+ if (!['preserve', 'light', 'substantive'].includes(options.strength) || !Number.isSafeInteger(maxPasses) || maxPasses < 0 || maxPasses > 100 || !Number.isSafeInteger(options.tokenBudget) || options.tokenBudget < 0 || !Number.isSafeInteger(options.perCallTokenReservation) || options.perCallTokenReservation < 1 || !Number.isFinite(options.timeBudgetMs) || options.timeBudgetMs <= 0)
58
+ throw new Error('Invalid revision limits');
59
+ const started = performance.now();
60
+ const result = { original, originalHash: hash(original), receivedProposals: [], candidates: [], best: original, bestHash: hash(original), receipt: {
61
+ strength: options.strength, passes: 0, stopReason: 'pass-limit', elapsedMs: 0, tokenBudget: options.tokenBudget, chargedTokens: 0, reportedTokens: 0, reservedTokens: 0, reportedCostUsd: null, costsComplete: true, calls: [], authorAcceptance: 'not-requested', qualityClaim: 'not-qualified',
62
+ } };
63
+ const remainingTime = () => options.timeBudgetMs - (performance.now() - started);
64
+ const call = async (phase, action) => {
65
+ if (options.signal?.aborted)
66
+ throw new RevisionStop('cancelled');
67
+ if (remainingTime() <= 0)
68
+ throw new RevisionStop('time-budget');
69
+ const reservation = options.perCallTokenReservation;
70
+ if (result.receipt.chargedTokens + reservation > options.tokenBudget)
71
+ throw new RevisionStop('token-budget');
72
+ const entry = { phase, measurement: 'reserved-upper-bound', chargedTokens: reservation };
73
+ result.receipt.calls.push(entry);
74
+ result.receipt.chargedTokens += reservation;
75
+ result.receipt.reservedTokens += reservation;
76
+ const controller = new AbortController();
77
+ let timer;
78
+ let abort;
79
+ try {
80
+ const context = { original, current: result.best, strength: options.strength, signal: controller.signal, maxTokens: reservation, ...(options.brief ? { brief: structuredClone(options.brief) } : {}) };
81
+ const response = await Promise.race([Promise.resolve().then(() => action(context)), new Promise((_, reject) => {
82
+ timer = setTimeout(() => { controller.abort(); reject(new RevisionStop('time-budget')); }, Math.max(1, remainingTime()));
83
+ abort = () => { controller.abort(); reject(new RevisionStop('cancelled')); };
84
+ options.signal?.addEventListener('abort', abort, { once: true });
85
+ })]);
86
+ if (!response || !('value' in response))
87
+ throw new RevisionStop('invalid-callback');
88
+ if (response.usage) {
89
+ const u = response.usage;
90
+ if (!Number.isSafeInteger(u.tokens) || u.tokens < 0 || !u.model?.trim() || !u.provider?.trim() || (u.costUsd !== undefined && (!Number.isFinite(u.costUsd) || u.costUsd < 0)))
91
+ throw new RevisionStop('invalid-usage');
92
+ entry.measurement = 'reported';
93
+ entry.chargedTokens = u.tokens;
94
+ entry.usage = structuredClone(u);
95
+ result.receipt.chargedTokens += u.tokens - reservation;
96
+ result.receipt.reservedTokens -= reservation;
97
+ result.receipt.reportedTokens += u.tokens;
98
+ if (u.costUsd !== undefined)
99
+ result.receipt.reportedCostUsd = (result.receipt.reportedCostUsd ?? 0) + u.costUsd;
100
+ if (u.tokens > reservation)
101
+ throw new RevisionStop('provider-budget-overrun');
102
+ }
103
+ if (options.signal?.aborted)
104
+ throw new RevisionStop('cancelled');
105
+ if (remainingTime() <= 0)
106
+ throw new RevisionStop('time-budget');
107
+ return structuredClone(response.value);
108
+ }
109
+ finally {
110
+ if (timer)
111
+ clearTimeout(timer);
112
+ if (abort)
113
+ options.signal?.removeEventListener('abort', abort);
114
+ if (entry.usage?.costUsd === undefined)
115
+ result.receipt.costsComplete = false;
116
+ }
117
+ };
118
+ try {
119
+ if (options.strength === 'preserve')
120
+ throw new RevisionStop('preserve');
121
+ if (!options.revise)
122
+ throw new RevisionStop('human-review');
123
+ for (let pass = 0; pass < maxPasses; pass++) {
124
+ const critique = options.critique ? await call('critique', options.critique) : [];
125
+ if (!Array.isArray(critique) || critique.some(c => !Number.isSafeInteger(c.start) || !Number.isSafeInteger(c.end) || c.start < 0 || c.end < c.start || c.end > result.best.length || !c.reason?.trim()))
126
+ throw new RevisionStop('invalid-critique');
127
+ const proposal = await call('revise', context => options.revise({ ...context, critique: structuredClone(critique) }));
128
+ result.receivedProposals.push({ parentHash: result.bestHash, payload: structuredClone(proposal) });
129
+ const review = createRevisionReview(result.best, proposal.edits, 'generated');
130
+ if (review.candidate !== proposal.candidate)
131
+ throw new RevisionStop('candidate-edit-mismatch');
132
+ result.receipt.passes++;
133
+ const candidate = { id: `candidate-${pass + 1}`, parentHash: result.bestHash, content: proposal.candidate, contentHash: hash(proposal.candidate), edits: review.edits, critique: structuredClone(critique), fidelity: 'uncertain', preference: 'unreviewed', retained: false };
134
+ result.candidates.push(candidate);
135
+ if (options.strength === 'light' && review.edits.some(e => /\r|\n/.test(e.expected + e.replacement) || !critique.some(c => c.start <= e.start && c.end >= e.end)))
136
+ throw new RevisionStop('strength-limit');
137
+ const assessment = assessWritingFidelity(original, proposal.candidate, options.brief);
138
+ candidate.fidelity = assessment.outcome;
139
+ candidate.fidelityAssessment = assessment;
140
+ if (assessment.outcome === 'fail')
141
+ throw new RevisionStop('fidelity-failure');
142
+ if (proposal.candidate === result.best) {
143
+ candidate.preference = 'same';
144
+ throw new RevisionStop('no-improvement');
145
+ }
146
+ if (!options.reviewCandidate)
147
+ throw new RevisionStop('human-review');
148
+ const judged = await call('review', context => options.reviewCandidate({ ...context, candidate: proposal.candidate }));
149
+ if (!['pass', 'fail', 'uncertain'].includes(judged.fidelity) || !['better', 'same', 'worse'].includes(judged.preference) || !judged.rationale?.trim())
150
+ throw new RevisionStop('invalid-review');
151
+ candidate.fidelity = judged.fidelity;
152
+ candidate.preference = judged.preference;
153
+ candidate.review = structuredClone(judged);
154
+ if (judged.fidelity !== 'pass')
155
+ throw new RevisionStop(judged.fidelity === 'fail' ? 'fidelity-failure' : 'fidelity-review');
156
+ if (judged.preference !== 'better')
157
+ throw new RevisionStop(judged.preference === 'same' ? 'no-improvement' : 'worse-candidate');
158
+ for (const previous of result.candidates)
159
+ previous.retained = false;
160
+ candidate.retained = true;
161
+ result.best = candidate.content;
162
+ result.bestHash = candidate.contentHash;
163
+ }
164
+ }
165
+ catch (error) {
166
+ result.receipt.stopReason = error instanceof RevisionStop ? error.reason : 'callback-or-validation-error';
167
+ }
168
+ result.receipt.elapsedMs = performance.now() - started;
169
+ return result;
170
+ }
171
+ /** Only human-origin corrections explicitly accepted by a person can propose expression overrides. */
172
+ export function proposeWriterLearning(profile, accepted, overrides) {
173
+ const p = parseWriterProfile(profile);
174
+ if (accepted.origin !== 'human' || accepted.approval !== 'explicit-human' || accepted.acceptedIds.length === 0 || accepted.output === accepted.original || overrides.length === 0)
175
+ throw new Error('Learning requires explicitly approved human corrections');
176
+ const checked = acceptRevisionEdits(accepted, accepted);
177
+ if (checked.output !== accepted.output || checked.outputHash !== accepted.outputHash)
178
+ throw new Error('Stale human correction artifact');
179
+ const validated = parseWriterProfile({ ...p, overrides: [...p.overrides, ...overrides] });
180
+ return { schemaVersion: 1, profileId: p.id, expectedRevision: p.revision, profileHash: hash(JSON.stringify(p)), overrides: validated.overrides.slice(p.overrides.length), provenance: { actor: accepted.actor, sourceHash: accepted.sourceHash, outputHash: accepted.outputHash, correctionIds: [...accepted.acceptedIds] } };
181
+ }
182
+ function advanceProfile(p) { const parts = p.version.split('.').map(Number); parts[2]++; p.version = parts.join('.'); p.revision++; p.cacheEpoch++; }
183
+ export function acceptWriterLearning(profile, proposal, decision) {
184
+ const p = parseWriterProfile(profile);
185
+ if (!decision.actor.trim() || proposal.schemaVersion !== 1 || p.id !== proposal.profileId || decision.expectedRevision !== p.revision || proposal.expectedRevision !== p.revision || proposal.profileHash !== hash(JSON.stringify(p)))
186
+ throw new Error('Stale profile update or missing explicit acceptance');
187
+ const previousOverrides = structuredClone(p.overrides);
188
+ p.overrides.push(...structuredClone(proposal.overrides));
189
+ advanceProfile(p);
190
+ const updated = parseWriterProfile(p);
191
+ return { profile: updated, undo: { acceptedBy: decision.actor, profileId: updated.id, afterHash: hash(JSON.stringify(updated)), previousOverrides, provenance: structuredClone(proposal.provenance) } };
192
+ }
193
+ export function undoWriterLearning(profile, undo) {
194
+ const p = parseWriterProfile(profile);
195
+ if (p.id !== undo.profileId || hash(JSON.stringify(p)) !== undo.afterHash)
196
+ throw new Error('Stale profile undo');
197
+ p.overrides = structuredClone(undo.previousOverrides);
198
+ advanceProfile(p);
199
+ return parseWriterProfile(p);
200
+ }
201
+ //# sourceMappingURL=voice-revision.js.map
@@ -0,0 +1,216 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { isAbsolute, resolve, join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { importLegacyWriterProfile } from './writer-profile-legacy.js';
6
+ import { parseWriterProfile } from './writer-profile.js';
7
+ import { WriterProfileStore } from './writer-profile-store.js';
8
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
9
+ const profileId = z.string().regex(/^[a-z0-9][a-z0-9.-]{0,79}$/).refine(value => !value.includes('..'));
10
+ const hash = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
11
+ function canonicalize(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(canonicalize);
14
+ if (value && typeof value === 'object') {
15
+ return Object.fromEntries(Object.entries(value)
16
+ .filter(([, child]) => child !== undefined)
17
+ .sort(([a], [b]) => a.localeCompare(b))
18
+ .map(([key, child]) => [key, canonicalize(child)]));
19
+ }
20
+ return value;
21
+ }
22
+ const requestSchema = z.object({
23
+ cwd: z.string().min(1),
24
+ sourcePath: z.string().min(1),
25
+ format: z.enum(['yaml', 'json']),
26
+ scope: z.enum(['project', 'user']).default('project'),
27
+ userConfigDir: z.string().min(1).optional(),
28
+ profile: z.object({
29
+ id: profileId,
30
+ name: z.string().min(1).max(200),
31
+ version: z.string().regex(/^\d+\.\d+\.\d+$/).default('1.0.0'),
32
+ provenance: z.object({ source: z.string().min(1).max(2000), license: z.string().min(1).max(200) }).strict(),
33
+ }).strict(),
34
+ }).strict();
35
+ const planSchema = z.object({
36
+ schemaVersion: z.literal('aiwg.writer-migration-plan.v1'),
37
+ id: z.string().regex(/^wm-[a-f0-9]{24}$/),
38
+ planSha256: digest,
39
+ createdAt: z.string().datetime(),
40
+ dryRun: z.literal(true),
41
+ cwd: z.string().min(1),
42
+ source: z.object({ path: z.string().min(1), format: z.enum(['yaml', 'json']), sha256: digest, bytes: z.number().int().nonnegative(), legacyKind: z.string().min(1).max(80) }).strict(),
43
+ target: z.object({
44
+ scope: z.enum(['project', 'user']),
45
+ profileId,
46
+ userConfigDir: z.string().min(1).optional(),
47
+ existingProfile: z.object({ revision: z.number().int().positive(), sha256: digest }).strict().nullable(),
48
+ activatesProfile: z.literal(false),
49
+ }).strict(),
50
+ actions: z.array(z.string().min(1).max(200)).min(1),
51
+ warnings: z.array(z.string().min(1).max(400)),
52
+ profileTemplate: z.unknown(),
53
+ }).strict();
54
+ function planDigest(plan) {
55
+ return hash(JSON.stringify(canonicalize(plan)));
56
+ }
57
+ function profileDigest(profile) {
58
+ return hash(JSON.stringify(canonicalize(profile)));
59
+ }
60
+ function resolveSourcePath(cwd, sourcePath) {
61
+ return isAbsolute(sourcePath) ? sourcePath : resolve(cwd, sourcePath);
62
+ }
63
+ function validateMigrationPlan(input) {
64
+ const parsed = planSchema.parse(input);
65
+ const profile = parseWriterProfile(parsed.profileTemplate);
66
+ const { planSha256, ...body } = parsed;
67
+ if (planDigest({ ...body, profileTemplate: profile }) !== planSha256)
68
+ throw new Error('Writer migration plan integrity mismatch');
69
+ if (profile.id !== parsed.target.profileId)
70
+ throw new Error('Writer migration plan target does not match profile template');
71
+ if (!profile.legacy || profile.legacy.sha256 !== parsed.source.sha256 || profile.legacy.format !== parsed.source.format || profile.legacy.kind !== parsed.source.legacyKind) {
72
+ throw new Error('Writer migration plan source does not match profile template legacy attachment');
73
+ }
74
+ return { ...parsed, profileTemplate: profile };
75
+ }
76
+ export async function planWriterProfileMigration(input) {
77
+ const request = requestSchema.parse(input);
78
+ const sourcePath = resolveSourcePath(request.cwd, request.sourcePath);
79
+ const raw = await readFile(sourcePath, 'utf8');
80
+ const legacy = importLegacyWriterProfile(raw, request.format);
81
+ const profile = parseWriterProfile({
82
+ schemaVersion: 1,
83
+ id: request.profile.id,
84
+ version: request.profile.version,
85
+ name: request.profile.name,
86
+ provenance: request.profile.provenance,
87
+ samples: [],
88
+ preferences: [],
89
+ legacy,
90
+ });
91
+ const store = new WriterProfileStore({ cwd: request.cwd, scope: request.scope, userConfigDir: request.userConfigDir });
92
+ let existingProfile = null;
93
+ try {
94
+ const current = await store.read(request.profile.id);
95
+ existingProfile = { revision: current.revision, sha256: profileDigest(current) };
96
+ }
97
+ catch (error) {
98
+ if (error.code !== 'ENOENT')
99
+ throw error;
100
+ }
101
+ const body = {
102
+ schemaVersion: 'aiwg.writer-migration-plan.v1',
103
+ id: `wm-${hash(`${sourcePath}\0${legacy.sha256}\0${request.profile.id}`).slice(0, 24)}`,
104
+ createdAt: new Date().toISOString(),
105
+ dryRun: true,
106
+ cwd: request.cwd,
107
+ source: { path: sourcePath, format: request.format, sha256: legacy.sha256, bytes: Buffer.byteLength(raw, 'utf8'), legacyKind: legacy.kind },
108
+ target: { scope: request.scope, profileId: request.profile.id, ...(request.userConfigDir ? { userConfigDir: request.userConfigDir } : {}), existingProfile, activatesProfile: false },
109
+ actions: ['validate legacy adapter payload', 'write private managed backup', 'create writer sidecar profile', 'leave output-mode activation unchanged'],
110
+ warnings: [
111
+ 'Migration preserves legacy numeric fields and raw bytes; it does not infer a replacement score.',
112
+ 'External shared exports may still exist outside this local profile store and require separate operator review.',
113
+ ],
114
+ profileTemplate: profile,
115
+ };
116
+ return { ...body, planSha256: planDigest(body) };
117
+ }
118
+ async function writePrivateBackup(store, profile, backup) {
119
+ const directory = store.managedMigrationBackupDirectory(profile);
120
+ await mkdir(directory, { recursive: true, mode: 0o700 });
121
+ const destination = join(directory, `${backup.id}.${randomUUID()}.json`);
122
+ const payload = JSON.stringify(backup, null, 2) + '\n';
123
+ const temporary = `${destination}.${randomUUID()}.tmp`;
124
+ try {
125
+ await writeFile(temporary, payload, { mode: 0o600, flag: 'wx' });
126
+ await rename(temporary, destination);
127
+ }
128
+ finally {
129
+ await rm(temporary, { force: true });
130
+ }
131
+ return { path: destination, sha256: hash(payload) };
132
+ }
133
+ export async function applyWriterProfileMigration(input) {
134
+ const plan = validateMigrationPlan(input);
135
+ const raw = await readFile(plan.source.path, 'utf8');
136
+ const legacy = importLegacyWriterProfile(raw, plan.source.format);
137
+ if (legacy.sha256 !== plan.source.sha256 || legacy.kind !== plan.source.legacyKind || Buffer.byteLength(raw, 'utf8') !== plan.source.bytes) {
138
+ throw new Error('Legacy profile changed after migration plan was created');
139
+ }
140
+ const store = new WriterProfileStore({ cwd: plan.cwd, scope: plan.target.scope, userConfigDir: plan.target.userConfigDir });
141
+ let previous = null;
142
+ try {
143
+ previous = await store.read(plan.target.profileId);
144
+ }
145
+ catch (error) {
146
+ if (error.code !== 'ENOENT')
147
+ throw error;
148
+ }
149
+ const currentState = previous ? { revision: previous.revision, sha256: profileDigest(previous) } : null;
150
+ if (JSON.stringify(currentState) !== JSON.stringify(plan.target.existingProfile))
151
+ throw new Error('Writer profile changed after migration plan was created');
152
+ const backup = {
153
+ schemaVersion: 'aiwg.writer-migration-backup.v1',
154
+ id: plan.id,
155
+ source: { path: plan.source.path, format: plan.source.format, sha256: plan.source.sha256, rawBase64: Buffer.from(raw, 'utf8').toString('base64') },
156
+ previousProfile: previous ? { sha256: profileDigest(previous), json: previous } : null,
157
+ };
158
+ const backupRecord = await writePrivateBackup(store, plan.target.profileId, backup);
159
+ const revisionBefore = previous?.revision ?? 0;
160
+ let saved;
161
+ try {
162
+ saved = await store.save(plan.profileTemplate, revisionBefore, { preserveMigrationBackups: true });
163
+ }
164
+ catch (error) {
165
+ await rm(backupRecord.path, { force: true });
166
+ throw error;
167
+ }
168
+ return {
169
+ schemaVersion: 'aiwg.writer-migration-apply.v1',
170
+ id: plan.id,
171
+ appliedAt: new Date().toISOString(),
172
+ cwd: plan.cwd,
173
+ sourceSha256: plan.source.sha256,
174
+ backupPath: backupRecord.path,
175
+ backupSha256: backupRecord.sha256,
176
+ profileId: saved.id,
177
+ scope: plan.target.scope,
178
+ ...(plan.target.userConfigDir ? { userConfigDir: plan.target.userConfigDir } : {}),
179
+ profileRevisionBefore: revisionBefore,
180
+ profileRevisionAfter: saved.revision,
181
+ createdProfileSha256: profileDigest(saved),
182
+ activatedProfile: false,
183
+ };
184
+ }
185
+ export async function rollbackWriterProfileMigration(result, options = {}) {
186
+ const cwd = options.cwd ?? result.cwd;
187
+ const store = new WriterProfileStore({ cwd, scope: options.scope ?? result.scope, userConfigDir: options.userConfigDir ?? result.userConfigDir });
188
+ const allowedDirectory = store.managedMigrationBackupDirectory(result.profileId);
189
+ if (!resolve(result.backupPath).startsWith(`${resolve(allowedDirectory)}/`))
190
+ throw new Error('Writer migration backup path is outside the managed backup directory');
191
+ const current = await store.read(result.profileId);
192
+ if (current.revision !== result.profileRevisionAfter || profileDigest(current) !== result.createdProfileSha256)
193
+ throw new Error('Writer profile changed after migration; rollback refused');
194
+ const payload = await readFile(result.backupPath, 'utf8');
195
+ if (hash(payload) !== result.backupSha256)
196
+ throw new Error('Writer migration backup integrity mismatch');
197
+ let backup;
198
+ try {
199
+ backup = JSON.parse(payload);
200
+ }
201
+ catch {
202
+ throw new Error('Writer migration backup is corrupt or not JSON');
203
+ }
204
+ if (backup.schemaVersion !== 'aiwg.writer-migration-backup.v1' || backup.id !== result.id || backup.source.sha256 !== result.sourceSha256)
205
+ throw new Error('Writer migration backup does not match apply result');
206
+ if (backup.previousProfile) {
207
+ const previous = parseWriterProfile(backup.previousProfile.json);
208
+ if (profileDigest(previous) !== backup.previousProfile.sha256)
209
+ throw new Error('Previous writer profile backup integrity mismatch');
210
+ await store.save(previous, current.revision);
211
+ return 'restored';
212
+ }
213
+ await store.delete(result.profileId, current.revision);
214
+ return 'removed';
215
+ }
216
+ //# sourceMappingURL=writer-migration.js.map
@@ -0,0 +1,145 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { parseDocument } from 'yaml';
4
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
5
+ const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
6
+ const finite = (value) => typeof value === 'number' && Number.isFinite(value);
7
+ const bounded = (value, max = 1) => finite(value) && value >= 0 && value <= max;
8
+ const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
9
+ const fail = () => { throw new Error('Invalid or unsupported legacy writer profile. Source text is omitted from diagnostics.'); };
10
+ /** Decode UTF-8 without dropping a BOM or silently replacing invalid source bytes. */
11
+ function sourceText(raw) {
12
+ if (typeof raw === 'string') {
13
+ if (Buffer.from(raw, 'utf8').toString('utf8') !== raw)
14
+ fail();
15
+ return raw;
16
+ }
17
+ if (!(raw instanceof Uint8Array))
18
+ fail();
19
+ const bytes = Buffer.from(raw);
20
+ const text = bytes.toString('utf8');
21
+ if (!Buffer.from(text, 'utf8').equals(bytes))
22
+ fail();
23
+ return text;
24
+ }
25
+ function parseSource(raw, format) {
26
+ try {
27
+ if (format === 'json')
28
+ return JSON.parse(raw.replace(/^\uFEFF/, ''));
29
+ if (format !== 'yaml')
30
+ fail();
31
+ const document = parseDocument(raw, { uniqueKeys: true, strict: true });
32
+ if (document.errors.length || document.warnings.length)
33
+ fail();
34
+ const payload = document.toJS({ maxAliasCount: 100 });
35
+ // Sidecars serialize as JSON: reject cyclic aliases, non-finite numbers and
36
+ // other YAML values that would silently change on private export.
37
+ if (!isDeepStrictEqual(payload, JSON.parse(JSON.stringify(payload))))
38
+ fail();
39
+ return payload;
40
+ }
41
+ catch {
42
+ // Parser messages may quote private samples, filenames or instruction text.
43
+ return fail();
44
+ }
45
+ }
46
+ function isCalibration(value) {
47
+ if (!record(value) || !nonempty(value.voice) || !bounded(value.detectionConfidence) || !record(value.characteristics) || !Array.isArray(value.markers))
48
+ return false;
49
+ const c = value.characteristics;
50
+ if (!['formality', 'technicality', 'assertiveness', 'complexity'].every(key => bounded(c[key])))
51
+ return false;
52
+ const lengths = c.sentenceLength;
53
+ if (!record(lengths) || !['avg', 'min', 'max', 'variance'].every(key => finite(lengths[key]) && lengths[key] >= 0))
54
+ return false;
55
+ if (!['basic', 'intermediate', 'advanced', 'expert'].includes(String(c.vocabularyLevel)) || !bounded(c.firstPersonUsage, 100) || !bounded(c.passiveVoiceRatio, 100))
56
+ return false;
57
+ return value.markers.every(marker => record(marker)
58
+ && ['vocabulary', 'structure', 'tone', 'perspective'].includes(String(marker.type))
59
+ && typeof marker.indicator === 'string' && bounded(marker.weight) && strings(marker.examples));
60
+ }
61
+ function isAnalyzer(value) {
62
+ if (!['academic', 'technical', 'executive', 'casual', 'mixed'].includes(String(value.primaryVoice)) || !bounded(value.confidence, 100) || !record(value.characteristics) || !record(value.metadata) || !Array.isArray(value.markers))
63
+ return false;
64
+ const characteristics = value.characteristics;
65
+ if (!['academic', 'technical', 'executive', 'casual'].every(key => finite(characteristics[key]) && characteristics[key] >= 0))
66
+ return false;
67
+ const metadata = value.metadata;
68
+ return ['wordCount', 'sentenceCount', 'averageSentenceLength'].every(key => finite(metadata[key]) && metadata[key] >= 0)
69
+ && ['first-person', 'third-person', 'neutral'].includes(String(value.perspective))
70
+ && ['formal', 'conversational', 'enthusiastic', 'matter-of-fact'].includes(String(value.tone))
71
+ && value.markers.every(marker => record(marker)
72
+ && ['academic', 'technical', 'executive', 'casual'].includes(String(marker.type))
73
+ && typeof marker.text === 'string' && Number.isInteger(marker.position) && marker.position >= 0
74
+ && ['strong', 'moderate', 'weak'].includes(String(marker.strength)));
75
+ }
76
+ /** Recognize producer families without rewriting their incompatible legacy fields. */
77
+ function classify(payload) {
78
+ if (Array.isArray(payload)) {
79
+ if (payload.length > 0 && payload.every(isCalibration))
80
+ return 'ts-calibration';
81
+ return fail();
82
+ }
83
+ if (!record(payload))
84
+ return fail();
85
+ if ('profiles' in payload) {
86
+ if (Array.isArray(payload.profiles) && payload.profiles.length > 0 && payload.profiles.every(isCalibration))
87
+ return 'ts-calibration';
88
+ return fail();
89
+ }
90
+ if ('primaryVoice' in payload)
91
+ return isAnalyzer(payload) ? 'ts-analyzer' : fail();
92
+ if ('voice' in payload)
93
+ return isCalibration(payload) ? 'ts-calibration' : fail();
94
+ if (!nonempty(payload.name) || !nonempty(payload.version) || typeof payload.description !== 'string' || !record(payload.tone))
95
+ return fail();
96
+ for (const key of ['formality', 'confidence', 'warmth', 'energy', 'complexity']) {
97
+ if (key in payload.tone && !bounded(payload.tone[key]))
98
+ return fail();
99
+ }
100
+ const producers = ['generated_from', 'analysis_source', 'blend_sources'].filter(key => key in payload);
101
+ if (producers.length > 1)
102
+ return fail();
103
+ if ('generated_from' in payload) {
104
+ if (typeof payload.generated_from !== 'string' || typeof payload.detected_domain !== 'string')
105
+ return fail();
106
+ return 'python-generated';
107
+ }
108
+ if ('analysis_source' in payload) {
109
+ const source = payload.analysis_source;
110
+ if (!record(source) || !finite(source.sample_size) || source.sample_size < 0 || !bounded(source.confidence) || !record(payload.extracted_metrics))
111
+ return fail();
112
+ return 'python-analyzed';
113
+ }
114
+ if ('blend_sources' in payload) {
115
+ if (!Array.isArray(payload.blend_sources) || !payload.blend_sources.length || !payload.blend_sources.every(source => record(source) && nonempty(source.name) && finite(source.weight)))
116
+ return fail();
117
+ return 'python-blended';
118
+ }
119
+ return 'addon-template';
120
+ }
121
+ /**
122
+ * Explicit, lossless import. No preferences, samples, identities or instructions
123
+ * are inferred. Keep this attachment private unless separately reviewed: legacy
124
+ * examples and unknown fields may contain personal text or secrets.
125
+ */
126
+ export function importLegacyWriterProfile(raw, format) {
127
+ const text = sourceText(raw);
128
+ const payload = parseSource(text, format);
129
+ const kind = classify(payload);
130
+ return { format, kind, raw: text, sha256: createHash('sha256').update(text, 'utf8').digest('hex'), payload };
131
+ }
132
+ /** Reject stale/tampered payloads and metadata without quoting source content. */
133
+ export function validateLegacyWriterProfile(attachment) {
134
+ if (!record(attachment) || typeof attachment.raw !== 'string')
135
+ return fail();
136
+ const imported = importLegacyWriterProfile(attachment.raw, attachment.format);
137
+ if (attachment.sha256 !== imported.sha256 || attachment.kind !== imported.kind || !isDeepStrictEqual(attachment.payload, imported.payload))
138
+ return fail();
139
+ return imported;
140
+ }
141
+ /** Return original UTF-8 text, including whitespace, comments, BOM and newlines. */
142
+ export function exportLegacyWriterProfile(attachment) {
143
+ return validateLegacyWriterProfile(attachment).raw;
144
+ }
145
+ //# sourceMappingURL=writer-profile-legacy.js.map