@aopslabs/aops 0.3.22 → 0.3.24

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 (42) hide show
  1. package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/docman.md +45 -0
  2. package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/manifest.json +5 -5
  3. package/agent-assets-release/agent-assets/inventory.json +5 -5
  4. package/agent-assets-release/agent-assets/projection.json +1 -1
  5. package/agent-assets-release/agent-assets-release.json +36 -36
  6. package/agent-assets-release/agent-assets-release.sigstore.json +25 -26
  7. package/assets/agent-assets/core/user-guides/docman.md +45 -0
  8. package/dist/commands/doc.js +561 -32
  9. package/dist/commands/global-update.js +447 -22
  10. package/dist/lib/agent-assets/native-fs.js +2 -2
  11. package/dist/lib/community-cockpit-lifecycle.js +95 -1
  12. package/dist/lib/community-native-lifecycle.js +54 -1
  13. package/native/bin/darwin-arm64/aops-agent-assets-fs +0 -0
  14. package/native/bin/darwin-x64/aops-agent-assets-fs +0 -0
  15. package/native/bin/linux-arm64/aops-agent-assets-fs +0 -0
  16. package/native/bin/linux-x64/aops-agent-assets-fs +0 -0
  17. package/native/bin/win32-x64/aops-agent-assets-fs.exe +0 -0
  18. package/native/manifest.json +6 -6
  19. package/native/qualifications/win32-x64.json +2 -2
  20. package/native/tui/darwin-arm64/aops-tui +0 -0
  21. package/native/tui/win32-x64/aops-tui.exe +0 -0
  22. package/package.json +7 -7
  23. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/agentspace/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/aops-cli-core/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/chatv3/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/collaborative-work/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/discuss/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/docman/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/mission/SKILL.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/projectman/SKILL.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/sys/SKILL.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/view/SKILL.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/references/working-disciplines/SKILL.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/agent-assets.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/agentspace.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/aops-cli.md +0 -0
  38. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/aops-system.md +0 -0
  39. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/chatv3.md +0 -0
  40. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/projectman.md +0 -0
  41. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/sys.md +0 -0
  42. /package/agent-assets-release/agent-assets/core/{47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a → 647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09}/files/user-guides/working-disciplines.md +0 -0
@@ -13,6 +13,7 @@ import { probeCliRuntimeMode } from '../utils/api.js';
13
13
  import { parseFrontmatterDocument } from '../utils/memory-workspace.js';
14
14
  const DEFAULT_DOC_BUILD_TIMEOUT_MS = 120_000;
15
15
  const DOC_MIRROR_CONTRACT = 'doc-mirror-v2';
16
+ export const DOC_SOURCE_MIRROR_CONTRACT = 'doc-mirror-v3';
16
17
  function collectRepeatedOption(value, previous = []) {
17
18
  const normalized = normalizeNonEmpty(value);
18
19
  return normalized ? [...previous, normalized] : previous;
@@ -288,6 +289,210 @@ function materializedContent(result) {
288
289
  function sha256Hex(content) {
289
290
  return createHash('sha256').update(content).digest('hex');
290
291
  }
292
+ function sha256ContentHash(content) {
293
+ return `sha256:${sha256Hex(content)}`;
294
+ }
295
+ export function validateSourceFileExtension(value, label = 'file extension') {
296
+ const extension = normalizeNonEmpty(value);
297
+ if (!extension || !/^[a-z0-9][a-z0-9_-]{0,31}$/.test(extension)) {
298
+ throw new Error(`${label} must be explicit lowercase ASCII without a leading dot (for example: cs, txt, or py).`);
299
+ }
300
+ return extension;
301
+ }
302
+ export function resolveDocMirrorVersionProfile(version, documentVersionId = '<unknown>') {
303
+ const rawContentMode = normalizeNonEmpty(version.contentMode)?.toLowerCase();
304
+ if (rawContentMode && rawContentMode !== 'structured' && rawContentMode !== 'text-file') {
305
+ throw new Error(`Docman version ${documentVersionId} has unsupported contentMode ${rawContentMode}.`);
306
+ }
307
+ const contentMode = rawContentMode === 'text-file' ? 'text-file' : 'structured';
308
+ const persistedExtension = normalizeNonEmpty(version.fileExtension);
309
+ if (contentMode === 'text-file') {
310
+ return {
311
+ contentMode,
312
+ fileExtension: validateSourceFileExtension(persistedExtension, `Document version ${documentVersionId} fileExtension`),
313
+ };
314
+ }
315
+ const fileExtension = persistedExtension
316
+ ? validateSourceFileExtension(persistedExtension, `Document version ${documentVersionId} fileExtension`)
317
+ : 'md';
318
+ if (fileExtension !== 'md') {
319
+ throw new Error(`Structured Docman version ${documentVersionId} declares persisted fileExtension ${fileExtension}; ` +
320
+ 'doc-mirror-v2 only supports md until an explicit structured mirror path migration contract exists.');
321
+ }
322
+ return { contentMode, fileExtension };
323
+ }
324
+ export function decodeSourceFileBuffer(buffer, sourcePath = '<source>') {
325
+ if (buffer.includes(0)) {
326
+ throw new Error(`Source file ${sourcePath} contains a binary NUL byte; text-file push stopped.`);
327
+ }
328
+ try {
329
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(buffer);
330
+ }
331
+ catch {
332
+ throw new Error(`Source file ${sourcePath} is not valid UTF-8; text-file push stopped.`);
333
+ }
334
+ }
335
+ async function readUtf8SourceFile(sourceInput) {
336
+ const source = normalizeNonEmpty(sourceInput);
337
+ if (!source)
338
+ throw new Error('Provide --source or --source-file.');
339
+ if (source === '-') {
340
+ throw new Error('Text-file source must be a filesystem path; stdin cannot provide an extension-safe mirror identity.');
341
+ }
342
+ const absolutePath = path.resolve(source.startsWith('@') ? source.slice(1) : source);
343
+ const buffer = await readFile(absolutePath);
344
+ const content = decodeSourceFileBuffer(buffer, absolutePath);
345
+ return { absolutePath, content, contentHash: sha256ContentHash(content) };
346
+ }
347
+ function requireSha256ContentHash(value, label) {
348
+ const contentHash = normalizeNonEmpty(value);
349
+ if (!contentHash || !/^sha256:[a-f0-9]{64}$/.test(contentHash)) {
350
+ throw new Error(`${label} must use sha256:<64 lowercase hex characters>.`);
351
+ }
352
+ return contentHash;
353
+ }
354
+ export function parseDocSourceMirrorSidecar(value, sidecarPath = '<sidecar>') {
355
+ if (!isRecord(value))
356
+ throw new Error(`Docman source sidecar ${sidecarPath} must contain a JSON object.`);
357
+ if (value.contract !== DOC_SOURCE_MIRROR_CONTRACT || value.schemaVersion !== 3) {
358
+ throw new Error(`Docman source sidecar ${sidecarPath} must use ${DOC_SOURCE_MIRROR_CONTRACT} schemaVersion 3.`);
359
+ }
360
+ if (value.entityType !== 'docman.document-source-mirror' ||
361
+ value.source !== 'docman' ||
362
+ value.readOnly !== false ||
363
+ value.contentMode !== 'text-file' ||
364
+ value.sourceFormat !== 'text') {
365
+ throw new Error(`Docman source sidecar ${sidecarPath} has an invalid v3 mirror identity.`);
366
+ }
367
+ const documentId = normalizeNonEmpty(value.documentId);
368
+ const documentVersionId = normalizeNonEmpty(value.documentVersionId);
369
+ const documentSlug = normalizeNonEmpty(value.documentSlug);
370
+ const title = normalizeNonEmpty(value.title);
371
+ const mediaType = normalizeNonEmpty(value.mediaType);
372
+ const mirrorPath = normalizeNonEmpty(value.mirrorPath);
373
+ const pulledAt = normalizeNonEmpty(value.pulledAt);
374
+ const documentVersion = Number(value.documentVersion);
375
+ if (!documentId || !documentVersionId || !documentSlug || !title || !mediaType || !mirrorPath || !pulledAt) {
376
+ throw new Error(`Docman source sidecar ${sidecarPath} is missing required mirror provenance.`);
377
+ }
378
+ if (!Number.isInteger(documentVersion) || documentVersion < 1) {
379
+ throw new Error(`Docman source sidecar ${sidecarPath} has an invalid documentVersion.`);
380
+ }
381
+ const fileExtension = validateSourceFileExtension(value.fileExtension, `Sidecar ${sidecarPath} fileExtension`);
382
+ const contentHash = requireSha256ContentHash(value.contentHash, `Sidecar ${sidecarPath} contentHash`);
383
+ return {
384
+ schemaVersion: 3,
385
+ entityType: 'docman.document-source-mirror',
386
+ contract: DOC_SOURCE_MIRROR_CONTRACT,
387
+ readOnly: false,
388
+ source: 'docman',
389
+ projectId: normalizeNonEmpty(value.projectId),
390
+ projectName: normalizeNonEmpty(value.projectName),
391
+ projectSlug: normalizeNonEmpty(value.projectSlug),
392
+ scopeId: normalizeNonEmpty(value.scopeId),
393
+ groupId: normalizeNonEmpty(value.groupId),
394
+ groupUid: normalizeNonEmpty(value.groupUid),
395
+ documentId,
396
+ documentVersionId,
397
+ documentVersion,
398
+ documentSlug,
399
+ title,
400
+ contentMode: 'text-file',
401
+ sourceFormat: 'text',
402
+ fileExtension,
403
+ mediaType,
404
+ contentHash,
405
+ mirrorPath,
406
+ pulledAt,
407
+ };
408
+ }
409
+ export async function readDocSourceMirrorSidecar(sidecarInput) {
410
+ const absolutePath = path.resolve(sidecarInput);
411
+ let content;
412
+ try {
413
+ content = await readFile(absolutePath, 'utf8');
414
+ }
415
+ catch (error) {
416
+ if (isFileSystemError(error, 'ENOENT')) {
417
+ throw new Error(`Docman source mirror sidecar is missing: ${absolutePath}. Push stopped.`);
418
+ }
419
+ throw error;
420
+ }
421
+ let parsed;
422
+ try {
423
+ parsed = JSON.parse(content);
424
+ }
425
+ catch {
426
+ throw new Error(`Docman source mirror sidecar is not valid JSON: ${absolutePath}.`);
427
+ }
428
+ return parseDocSourceMirrorSidecar(parsed, absolutePath);
429
+ }
430
+ function parseSourceMaterializedPayload(result, expectedVersionId) {
431
+ const payload = unwrapResultData(result);
432
+ const documentVersionId = normalizeNonEmpty(payload?.documentVersionId);
433
+ if (documentVersionId !== expectedVersionId) {
434
+ throw new Error(`Docman source materialize returned version ${documentVersionId ?? '<missing>'}; expected ${expectedVersionId}.`);
435
+ }
436
+ const content = payload?.content;
437
+ if (typeof content !== 'string')
438
+ throw new Error('Docman source materialize did not return textual content.');
439
+ if (payload?.contentMode !== 'text-file' || payload.sourceFormat !== 'text') {
440
+ throw new Error('Docman source materialize did not return a text-file/text payload.');
441
+ }
442
+ const fileExtension = validateSourceFileExtension(payload.fileExtension, 'Materialized fileExtension');
443
+ const contentHash = requireSha256ContentHash(payload.contentHash, 'Materialized contentHash');
444
+ if (contentHash !== sha256ContentHash(content)) {
445
+ throw new Error('Docman source materialize contentHash does not match the exact UTF-8 content.');
446
+ }
447
+ const mediaType = normalizeNonEmpty(payload.mediaType);
448
+ const suggestedFileName = normalizeNonEmpty(payload.suggestedFileName);
449
+ if (!mediaType || !suggestedFileName) {
450
+ throw new Error('Docman source materialize is missing mediaType or suggestedFileName.');
451
+ }
452
+ return {
453
+ documentVersionId,
454
+ content,
455
+ contentHash,
456
+ contentMode: 'text-file',
457
+ sourceFormat: 'text',
458
+ fileExtension,
459
+ mediaType,
460
+ suggestedFileName,
461
+ };
462
+ }
463
+ export function validateDocSourceMirrorPushBaseline(params) {
464
+ const filenameExtension = path.extname(params.sourcePath).slice(1);
465
+ if (filenameExtension !== params.sidecar.fileExtension) {
466
+ throw new Error(`Source filename extension ${filenameExtension || '<missing>'} does not match persisted sidecar extension ${params.sidecar.fileExtension}.`);
467
+ }
468
+ if (path.basename(params.sidecar.mirrorPath) !== path.basename(params.sourcePath)) {
469
+ throw new Error(`Source filename ${path.basename(params.sourcePath)} does not match sidecar mirrorPath ${params.sidecar.mirrorPath}.`);
470
+ }
471
+ if (params.sidecar.scopeId && params.resolvedScopeId && params.sidecar.scopeId !== params.resolvedScopeId) {
472
+ throw new Error(`Source sidecar scope ${params.sidecar.scopeId} does not match resolved scope ${params.resolvedScopeId}.`);
473
+ }
474
+ if (params.sidecar.projectId && params.resolvedProjectId && params.sidecar.projectId !== params.resolvedProjectId) {
475
+ throw new Error(`Source sidecar project ${params.sidecar.projectId} does not match resolved project ${params.resolvedProjectId}.`);
476
+ }
477
+ const currentVersionId = recordId(params.currentVersion, 'documentVersionId');
478
+ if (currentVersionId !== params.sidecar.documentVersionId) {
479
+ throw new Error(`Stale source sidecar version ${params.sidecar.documentVersionId}; hosted current version is ${currentVersionId ?? '<missing>'}.`);
480
+ }
481
+ if (normalizeNonEmpty(params.currentVersion.contentMode) !== 'text-file') {
482
+ throw new Error(`Hosted current version ${currentVersionId} is not a text-file document version.`);
483
+ }
484
+ const currentExtension = validateSourceFileExtension(params.currentVersion.fileExtension, `Hosted current version ${currentVersionId} fileExtension`);
485
+ if (currentExtension !== params.sidecar.fileExtension) {
486
+ throw new Error(`Stale source sidecar extension ${params.sidecar.fileExtension}; hosted current extension is ${currentExtension}.`);
487
+ }
488
+ if (params.hostedSource.fileExtension !== params.sidecar.fileExtension) {
489
+ throw new Error(`Stale source sidecar extension ${params.sidecar.fileExtension}; materialized extension is ${params.hostedSource.fileExtension}.`);
490
+ }
491
+ if (params.hostedSource.contentHash !== params.sidecar.contentHash) {
492
+ throw new Error(`Stale source sidecar hash ${params.sidecar.contentHash}; hosted current hash is ${params.hostedSource.contentHash}.`);
493
+ }
494
+ return params.sourceContentHash === params.sidecar.contentHash ? 'up-to-date' : 'create-version';
495
+ }
291
496
  function pathSegmentSlug(value, fallback) {
292
497
  const normalized = normalizeNonEmpty(value) ?? fallback;
293
498
  const safe = normalized
@@ -1574,6 +1779,67 @@ function buildDocMirrorFile(params) {
1574
1779
  content: `${frontmatter}<!-- READ-ONLY MIRROR: update Docman/source docs, then run aops-cli doc mirror pull. -->\n\n${params.content}`,
1575
1780
  };
1576
1781
  }
1782
+ export function buildDocSourceMirrorArtifacts(params) {
1783
+ const documentId = recordId(params.document, 'documentId');
1784
+ const documentVersionId = recordId(params.documentVersion, 'documentVersionId');
1785
+ const documentSlug = normalizeNonEmpty(params.document.slug) ??
1786
+ pathSegmentSlug(normalizeNonEmpty(params.document.title), documentId ?? 'document');
1787
+ const title = normalizeNonEmpty(params.document.title) ?? documentSlug;
1788
+ const version = recordVersion(params.documentVersion);
1789
+ if (!documentId || !documentVersionId || version < 1) {
1790
+ throw new Error('Text-file mirror plan is missing document/version provenance.');
1791
+ }
1792
+ const fileExtension = validateSourceFileExtension(params.materialized.fileExtension);
1793
+ if (params.materialized.contentHash !== sha256ContentHash(params.materialized.content)) {
1794
+ throw new Error('Docman source mirror artifact contentHash does not match the exact raw UTF-8 content.');
1795
+ }
1796
+ if (!params.materialized.suggestedFileName.endsWith(`.${fileExtension}`)) {
1797
+ throw new Error(`Docman suggested source filename ${params.materialized.suggestedFileName} conflicts with persisted extension ${fileExtension}.`);
1798
+ }
1799
+ const mirrorPath = path.join(params.groupPath, `${pathSegmentSlug(documentSlug, 'document')}.${fileExtension}`);
1800
+ const rawPath = path.join(params.outputRoot, mirrorPath);
1801
+ const sidecarPath = `${rawPath}.docman.json`;
1802
+ const sidecar = {
1803
+ schemaVersion: 3,
1804
+ entityType: 'docman.document-source-mirror',
1805
+ contract: DOC_SOURCE_MIRROR_CONTRACT,
1806
+ readOnly: false,
1807
+ source: 'docman',
1808
+ projectId: params.resolvedContext.projectId,
1809
+ projectName: params.resolvedContext.projectName,
1810
+ projectSlug: params.resolvedContext.projectSlug,
1811
+ scopeId: params.resolvedContext.scopeId,
1812
+ groupId: normalizeNonEmpty(params.document.groupId),
1813
+ groupUid: normalizeNonEmpty(params.document.groupUid),
1814
+ documentId,
1815
+ documentVersionId,
1816
+ documentVersion: version,
1817
+ documentSlug,
1818
+ title,
1819
+ contentMode: 'text-file',
1820
+ sourceFormat: 'text',
1821
+ fileExtension,
1822
+ mediaType: params.materialized.mediaType,
1823
+ contentHash: params.materialized.contentHash,
1824
+ mirrorPath: mirrorPath.split(path.sep).join('/'),
1825
+ pulledAt: new Date().toISOString(),
1826
+ };
1827
+ return {
1828
+ raw: { filePath: rawPath, content: params.materialized.content },
1829
+ sidecar: { filePath: sidecarPath, content: `${JSON.stringify(sidecar, null, 2)}\n` },
1830
+ entry: {
1831
+ title,
1832
+ mirrorPath: sidecar.mirrorPath,
1833
+ sidecarPath: path.relative(params.outputRoot, sidecarPath).split(path.sep).join('/'),
1834
+ documentId,
1835
+ documentVersionId,
1836
+ documentVersion: version,
1837
+ contentMode: 'text-file',
1838
+ fileExtension,
1839
+ contentHash: sidecar.contentHash,
1840
+ },
1841
+ };
1842
+ }
1577
1843
  function buildDocMirrorIndex(params) {
1578
1844
  const indexPath = path.join(params.outputRoot, 'index.md');
1579
1845
  const frontmatter = renderFrontmatter({
@@ -1591,7 +1857,7 @@ function buildDocMirrorIndex(params) {
1591
1857
  frontmatter,
1592
1858
  '# Docman Mirror Index',
1593
1859
  '',
1594
- 'This directory is a read-only mirror of hosted Docman documents. Edit canonical Docman/source docs, then run `aops-cli doc mirror pull --apply`.',
1860
+ 'This directory mirrors hosted Docman documents. Structured v2 `.md` files are read-only; text-file v3 entries are editable only with their adjacent `.docman.json` sidecar and guarded `aops-cli doc mirror push --source-file <path>`.',
1595
1861
  '',
1596
1862
  ...params.entries.map((entry) => {
1597
1863
  const title = normalizeNonEmpty(entry.title) ?? 'Untitled';
@@ -1678,11 +1944,47 @@ export async function commitStagedMirrorFiles(files) {
1678
1944
  async function listDocMirrorIndexEntries(outputRoot) {
1679
1945
  const files = await listLocalMirrorMarkdownFiles(outputRoot);
1680
1946
  const docs = await readLocalMirrorDocumentsFromFiles(outputRoot, files);
1681
- return docs.map((doc) => ({
1947
+ const structured = docs.map((doc) => ({
1682
1948
  title: doc.title,
1683
1949
  mirrorPath: doc.relativePath,
1684
1950
  documentVersion: doc.documentVersion,
1685
1951
  }));
1952
+ return [...structured, ...(await listDocSourceMirrorIndexEntries(outputRoot))];
1953
+ }
1954
+ async function listDocSourceMirrorSidecarFiles(root) {
1955
+ try {
1956
+ const entries = await readdir(root, { withFileTypes: true });
1957
+ const nested = await Promise.all(entries.map(async (entry) => {
1958
+ const fullPath = path.join(root, entry.name);
1959
+ if (entry.isDirectory() && !entry.name.startsWith('.'))
1960
+ return listDocSourceMirrorSidecarFiles(fullPath);
1961
+ if (entry.isFile() && entry.name.endsWith('.docman.json'))
1962
+ return [fullPath];
1963
+ return [];
1964
+ }));
1965
+ return nested.flat().sort();
1966
+ }
1967
+ catch {
1968
+ return [];
1969
+ }
1970
+ }
1971
+ async function listDocSourceMirrorIndexEntries(outputRoot) {
1972
+ const entries = [];
1973
+ for (const sidecarPath of await listDocSourceMirrorSidecarFiles(outputRoot)) {
1974
+ const sidecar = await readDocSourceMirrorSidecar(sidecarPath);
1975
+ entries.push({
1976
+ title: sidecar.title,
1977
+ mirrorPath: sidecar.mirrorPath,
1978
+ sidecarPath: path.relative(outputRoot, sidecarPath).split(path.sep).join('/'),
1979
+ documentId: sidecar.documentId,
1980
+ documentVersionId: sidecar.documentVersionId,
1981
+ documentVersion: sidecar.documentVersion,
1982
+ contentMode: sidecar.contentMode,
1983
+ fileExtension: sidecar.fileExtension,
1984
+ contentHash: sidecar.contentHash,
1985
+ });
1986
+ }
1987
+ return entries;
1686
1988
  }
1687
1989
  function localMirrorLimit(value) {
1688
1990
  const parsed = toInteger(value, '--limit');
@@ -1702,8 +2004,15 @@ async function listLocalMirrorMarkdownFiles(root) {
1702
2004
  const fullPath = path.join(root, entry.name);
1703
2005
  if (entry.isDirectory() && !entry.name.startsWith('.'))
1704
2006
  return listLocalMirrorMarkdownFiles(fullPath);
1705
- if (entry.isFile() && entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'index.md')
1706
- return [fullPath];
2007
+ if (entry.isFile() && entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'index.md') {
2008
+ try {
2009
+ await stat(`${fullPath}.docman.json`);
2010
+ return [];
2011
+ }
2012
+ catch {
2013
+ return [fullPath];
2014
+ }
2015
+ }
1707
2016
  return [];
1708
2017
  }));
1709
2018
  return nested.flat().sort();
@@ -2320,12 +2629,172 @@ export async function runDocImportMarkdown(options = {}) {
2320
2629
  process.exitCode = 1;
2321
2630
  }
2322
2631
  }
2632
+ export async function runDocSourceFileSave(options = {}) {
2633
+ try {
2634
+ const documentId = normalizeNonEmpty(options.documentId);
2635
+ if (!documentId)
2636
+ throw new Error('Provide --document-id.');
2637
+ const source = await readUtf8SourceFile(options.source);
2638
+ const fileExtension = validateSourceFileExtension(options.extension, '--extension');
2639
+ const expectedPreviousContentHash = options.expectedContentHash === undefined
2640
+ ? undefined
2641
+ : requireSha256ContentHash(options.expectedContentHash, '--expected-content-hash');
2642
+ const apiState = await requireApiState(options);
2643
+ if (!apiState)
2644
+ return;
2645
+ const resolvedContext = await resolveDocContext(options, apiState, { forceHostedScope: true });
2646
+ const auditFields = await resolveDocAuditFields(apiState, options, {}, 'create');
2647
+ const input = compactPayload({
2648
+ documentId,
2649
+ content: source.content,
2650
+ fileExtension,
2651
+ title: normalizeNonEmpty(options.title),
2652
+ expectedPreviousVersionId: normalizeNonEmpty(options.expectedVersionId),
2653
+ expectedPreviousContentHash,
2654
+ publishNow: options.publishNow === true,
2655
+ ...auditFields,
2656
+ });
2657
+ const previewInput = compactPayload({
2658
+ ...input,
2659
+ content: undefined,
2660
+ sourcePath: source.absolutePath,
2661
+ sourceContentHash: source.contentHash,
2662
+ sourceBytes: Buffer.byteLength(source.content, 'utf8'),
2663
+ });
2664
+ if (await guardDocWrite({
2665
+ options,
2666
+ command: 'doc.source-file.save',
2667
+ surface: 'docman.document.source-file.save',
2668
+ resolvedContext,
2669
+ input: previewInput,
2670
+ }))
2671
+ return;
2672
+ const result = await invokeDocCrudMutationRaw(apiState, options, resolvedContext, 'docman.document.source-file.save', input, { idempotencyKey: normalizeNonEmpty(options.idempotencyKey) });
2673
+ await emitDocResult({
2674
+ options,
2675
+ command: 'doc.source-file.save',
2676
+ surface: 'docman.document.source-file.save',
2677
+ resolvedContext,
2678
+ input: previewInput,
2679
+ result,
2680
+ artifacts: compactPayload({
2681
+ documentId,
2682
+ documentVersionId: normalizeNonEmpty(unwrapCrudData(result).documentVersionId),
2683
+ contentHash: normalizeNonEmpty(unwrapCrudData(result).contentHash),
2684
+ }),
2685
+ successText: options.publishNow === true
2686
+ ? 'Docman source-file snapshot saved and published.'
2687
+ : 'Docman source-file draft snapshot saved.',
2688
+ });
2689
+ }
2690
+ catch (error) {
2691
+ logError(error instanceof Error ? error.message : String(error));
2692
+ process.exitCode = 1;
2693
+ }
2694
+ }
2695
+ async function runDocSourceMirrorPush(options, apiState, resolvedContext) {
2696
+ const source = await readUtf8SourceFile(options.sourceFile);
2697
+ const sidecarPath = path.resolve(normalizeNonEmpty(options.sidecar) ?? `${source.absolutePath}.docman.json`);
2698
+ const sidecar = await readDocSourceMirrorSidecar(sidecarPath);
2699
+ const versions = await listDocumentVersions(apiState, options, resolvedContext, sidecar.documentId);
2700
+ const current = selectCurrentPublishedDocumentVersion(versions, sidecar.documentId);
2701
+ const hostedMaterialized = await callDocmanVersionRoute(apiState, options, resolvedContext, {
2702
+ documentVersionId: sidecar.documentVersionId,
2703
+ suffix: 'materialize',
2704
+ method: 'POST',
2705
+ body: compactPayload({
2706
+ documentVersionId: sidecar.documentVersionId,
2707
+ target: 'source',
2708
+ locale: normalizeNonEmpty(options.locale),
2709
+ fallbackLocale: normalizeNonEmpty(options.fallbackLocale),
2710
+ }),
2711
+ });
2712
+ const hostedSource = parseSourceMaterializedPayload(hostedMaterialized, sidecar.documentVersionId);
2713
+ const action = validateDocSourceMirrorPushBaseline({
2714
+ sourcePath: source.absolutePath,
2715
+ sourceContentHash: source.contentHash,
2716
+ sidecar,
2717
+ currentVersion: current,
2718
+ hostedSource,
2719
+ resolvedScopeId: normalizeNonEmpty(resolvedContext.scopeId),
2720
+ resolvedProjectId: normalizeNonEmpty(resolvedContext.projectId),
2721
+ });
2722
+ const auditFields = await resolveDocAuditFields(apiState, options, {}, 'create');
2723
+ const saveInput = compactPayload({
2724
+ documentId: sidecar.documentId,
2725
+ content: source.content,
2726
+ fileExtension: sidecar.fileExtension,
2727
+ title: normalizeNonEmpty(options.title) ?? sidecar.title,
2728
+ expectedPreviousVersionId: sidecar.documentVersionId,
2729
+ expectedPreviousContentHash: sidecar.contentHash,
2730
+ publishNow: options.publishNow === true,
2731
+ ...auditFields,
2732
+ });
2733
+ const input = {
2734
+ sourcePath: source.absolutePath,
2735
+ sidecarPath,
2736
+ action,
2737
+ sourceContentHash: source.contentHash,
2738
+ sourceBytes: Buffer.byteLength(source.content, 'utf8'),
2739
+ documentId: sidecar.documentId,
2740
+ expectedPreviousVersionId: sidecar.documentVersionId,
2741
+ expectedPreviousContentHash: sidecar.contentHash,
2742
+ fileExtension: sidecar.fileExtension,
2743
+ publishNow: options.publishNow === true,
2744
+ };
2745
+ if (await guardDocWrite({
2746
+ options,
2747
+ command: 'doc.mirror.push',
2748
+ surface: `${DOC_SOURCE_MIRROR_CONTRACT} guarded source push`,
2749
+ resolvedContext,
2750
+ input,
2751
+ }))
2752
+ return;
2753
+ if (action === 'up-to-date') {
2754
+ await emitDocResult({
2755
+ options,
2756
+ command: 'doc.mirror.push',
2757
+ surface: `${DOC_SOURCE_MIRROR_CONTRACT} guarded source push`,
2758
+ resolvedContext,
2759
+ input,
2760
+ result: { action, changed: false, documentId: sidecar.documentId, documentVersionId: sidecar.documentVersionId },
2761
+ successText: 'Docman source mirror is already up to date.',
2762
+ });
2763
+ return;
2764
+ }
2765
+ const result = await invokeDocCrudMutationRaw(apiState, options, resolvedContext, 'docman.document.source-file.save', saveInput, { idempotencyKey: normalizeNonEmpty(options.idempotencyKey) });
2766
+ await emitDocResult({
2767
+ options,
2768
+ command: 'doc.mirror.push',
2769
+ surface: `${DOC_SOURCE_MIRROR_CONTRACT} guarded source push`,
2770
+ resolvedContext,
2771
+ input,
2772
+ result,
2773
+ artifacts: compactPayload({
2774
+ sourcePath: source.absolutePath,
2775
+ sidecarPath,
2776
+ documentId: sidecar.documentId,
2777
+ documentVersionId: normalizeNonEmpty(unwrapCrudData(result).documentVersionId),
2778
+ contentHash: normalizeNonEmpty(unwrapCrudData(result).contentHash),
2779
+ }),
2780
+ successText: options.publishNow === true
2781
+ ? 'Docman source mirror snapshot pushed and published.'
2782
+ : 'Docman source mirror draft snapshot pushed; run pull only after publishing it current.',
2783
+ });
2784
+ }
2323
2785
  export async function runDocMirrorPush(options = {}) {
2324
2786
  try {
2325
2787
  const apiState = await requireApiState(options);
2326
2788
  if (!apiState)
2327
2789
  return;
2328
2790
  const resolvedContext = await resolveDocContext(options, apiState, { forceHostedScope: true });
2791
+ if (normalizeNonEmpty(options.sourceFile)) {
2792
+ await runDocSourceMirrorPush(options, apiState, resolvedContext);
2793
+ return;
2794
+ }
2795
+ if (normalizeNonEmpty(options.sidecar)) {
2796
+ throw new Error('--sidecar requires --source-file.');
2797
+ }
2329
2798
  const groupUid = normalizeNonEmpty(options.groupUid) ?? 'architecture';
2330
2799
  const groupTitle = normalizeNonEmpty(options.groupTitle) ?? 'Architecture';
2331
2800
  const sources = await readRootMarkdownSources(options.sourceDir);
@@ -2581,7 +3050,11 @@ export async function runDocMirrorPull(options = {}) {
2581
3050
  const groupPath = buildGroupPath(document, groupsById, groupsByUid);
2582
3051
  const documentSlug = normalizeNonEmpty(document.slug) ??
2583
3052
  pathSegmentSlug(normalizeNonEmpty(document.title), normalizeNonEmpty(documentId) ?? 'document');
2584
- const mirrorPath = path.join(groupPath, `${pathSegmentSlug(documentSlug, 'document')}.md`);
3053
+ const profile = resolveDocMirrorVersionProfile(latest, documentVersionId);
3054
+ const { contentMode } = profile;
3055
+ const fileExtension = contentMode === 'text-file' ? profile.fileExtension : undefined;
3056
+ const materializeTarget = contentMode === 'text-file' ? 'source' : target;
3057
+ const mirrorPath = path.join(groupPath, `${pathSegmentSlug(documentSlug, 'document')}.${fileExtension ?? 'md'}`);
2585
3058
  const canonicalMirrorPath = path.resolve(outputRoot, mirrorPath).toLowerCase();
2586
3059
  if (seenMirrorPaths.has(canonicalMirrorPath)) {
2587
3060
  throw new Error(`Docman mirror plan contains duplicate output path ${mirrorPath}.`);
@@ -2595,6 +3068,9 @@ export async function runDocMirrorPull(options = {}) {
2595
3068
  slug: documentSlug,
2596
3069
  groupPath,
2597
3070
  mirrorPath,
3071
+ contentMode,
3072
+ fileExtension,
3073
+ target: materializeTarget,
2598
3074
  });
2599
3075
  }
2600
3076
  const input = compactPayload({
@@ -2623,14 +3099,23 @@ export async function runDocMirrorPull(options = {}) {
2623
3099
  const document = documents.find((entry) => recordId(entry, 'documentId') === documentId);
2624
3100
  if (!document)
2625
3101
  throw new Error(`Validated mirror plan references unknown document ${documentId}.`);
2626
- const documentVersion = { id: documentVersionId, documentId, version: plan.documentVersion, status: 'published', isCurrent: true };
3102
+ const documentVersion = {
3103
+ id: documentVersionId,
3104
+ documentId,
3105
+ version: plan.documentVersion,
3106
+ status: 'published',
3107
+ isCurrent: true,
3108
+ contentMode: plan.contentMode,
3109
+ fileExtension: plan.fileExtension,
3110
+ };
3111
+ const materializeTarget = normalizeNonEmpty(plan.target) ?? target;
2627
3112
  const materialized = await callDocmanVersionRoute(apiState, options, resolvedContext, {
2628
3113
  documentVersionId,
2629
3114
  suffix: 'materialize',
2630
3115
  method: 'POST',
2631
3116
  body: compactPayload({
2632
3117
  documentVersionId,
2633
- target,
3118
+ target: materializeTarget,
2634
3119
  locale: normalizeNonEmpty(options.locale),
2635
3120
  fallbackLocale: normalizeNonEmpty(options.fallbackLocale),
2636
3121
  }),
@@ -2641,27 +3126,54 @@ export async function runDocMirrorPull(options = {}) {
2641
3126
  throw new Error(`Docman materialize returned version ${returnedVersionId ?? '<missing>'}; expected ${documentVersionId}. ` +
2642
3127
  'Mirror pull stopped before replacing any destination file.');
2643
3128
  }
2644
- const content = materializedContent(materialized);
2645
- materializedPlans.push({ plan, document, documentVersion, content });
3129
+ if (plan.contentMode === 'text-file') {
3130
+ const source = parseSourceMaterializedPayload(materialized, documentVersionId);
3131
+ if (source.fileExtension !== plan.fileExtension) {
3132
+ throw new Error(`Docman source materialize extension ${source.fileExtension} differs from persisted version extension ${String(plan.fileExtension)}.`);
3133
+ }
3134
+ }
3135
+ else {
3136
+ materializedContent(materialized);
3137
+ }
3138
+ materializedPlans.push({ plan, document, documentVersion, materialized });
2646
3139
  }
2647
3140
  const existingIndexEntries = await listDocMirrorIndexEntries(outputRoot);
2648
3141
  const written = [];
2649
- const artifacts = materializedPlans.map(({ plan, document, documentVersion, content }) => {
2650
- const artifact = buildDocMirrorFile({
2651
- outputRoot,
2652
- groupPath: normalizeNonEmpty(plan.groupPath) ?? 'ungrouped',
2653
- document,
2654
- documentVersion,
2655
- resolvedContext,
2656
- target,
2657
- content,
2658
- });
2659
- written.push({
2660
- ...plan,
2661
- outputPath: artifact.filePath,
2662
- });
2663
- return artifact;
2664
- });
3142
+ const artifacts = [];
3143
+ for (const { plan, document, documentVersion, materialized } of materializedPlans) {
3144
+ if (plan.contentMode === 'text-file') {
3145
+ const sourceArtifacts = buildDocSourceMirrorArtifacts({
3146
+ outputRoot,
3147
+ groupPath: normalizeNonEmpty(plan.groupPath) ?? 'ungrouped',
3148
+ document,
3149
+ documentVersion,
3150
+ resolvedContext,
3151
+ materialized: parseSourceMaterializedPayload(materialized, normalizeNonEmpty(plan.documentVersionId) ?? ''),
3152
+ });
3153
+ artifacts.push(sourceArtifacts.raw, sourceArtifacts.sidecar);
3154
+ written.push({
3155
+ ...plan,
3156
+ ...sourceArtifacts.entry,
3157
+ outputPath: sourceArtifacts.raw.filePath,
3158
+ });
3159
+ }
3160
+ else {
3161
+ const artifact = buildDocMirrorFile({
3162
+ outputRoot,
3163
+ groupPath: normalizeNonEmpty(plan.groupPath) ?? 'ungrouped',
3164
+ document,
3165
+ documentVersion,
3166
+ resolvedContext,
3167
+ target: normalizeNonEmpty(plan.target) ?? target,
3168
+ content: materializedContent(materialized),
3169
+ });
3170
+ artifacts.push(artifact);
3171
+ written.push({
3172
+ ...plan,
3173
+ outputPath: artifact.filePath,
3174
+ });
3175
+ }
3176
+ }
2665
3177
  const indexEntriesByPath = new Map();
2666
3178
  for (const entry of existingIndexEntries) {
2667
3179
  const mirrorPath = normalizeNonEmpty(entry.mirrorPath);
@@ -4758,8 +5270,8 @@ export async function runDocPublish(options = {}) {
4758
5270
  const resolvedContext = await resolveDocContext(options, apiState);
4759
5271
  const documentVersionId = requireDocumentVersionId(options);
4760
5272
  const target = normalizeNonEmpty(options.target) ?? 'markdown';
4761
- if (target !== 'markdown' && target !== 'html') {
4762
- throw new Error('Target must be markdown or html.');
5273
+ if (target !== 'markdown' && target !== 'html' && target !== 'source') {
5274
+ throw new Error('Target must be markdown, html, or source.');
4763
5275
  }
4764
5276
  const input = compactPayload({
4765
5277
  documentVersionId,
@@ -5305,11 +5817,28 @@ export function makeDocCommand() {
5305
5817
  'A dry-run returns a short-lived planToken. Pass it to --apply --plan-token <token> to reject source or graph drift; apply without a token remains supported.',
5306
5818
  ],
5307
5819
  }));
5308
- const mirror = cmd.command('mirror').description('Docman mirror pull and markdown import helpers');
5820
+ const sourceFile = cmd.command('source-file').description('Immutable Docman text-file snapshot commands');
5821
+ applyDocContextOptions(applyWriteOptions(sourceFile
5822
+ .command('save')
5823
+ .description('Save an exact UTF-8 source file as a new immutable text-file document version')
5824
+ .requiredOption('--document-id <id>', 'Existing Docman document id')
5825
+ .requiredOption('--source <path>', 'UTF-8 source file path')
5826
+ .requiredOption('--extension <extension>', 'Explicit persisted lowercase extension without a leading dot')
5827
+ .option('--title <title>', 'Snapshot/page title')
5828
+ .option('--expected-version-id <id>', 'Reject unless this remains the previous current/latest version')
5829
+ .option('--expected-content-hash <hash>', 'Reject unless previous source content still has this sha256 hash')
5830
+ .option('--publish-now', 'Atomically publish/select the new snapshot as current')).action(async (options) => {
5831
+ await runDocSourceFileSave(options);
5832
+ }));
5833
+ const mirror = cmd.command('mirror').description('Docman v2 structured and v3 source-file mirror helpers');
5309
5834
  applyDocContextOptions(applyWriteOptions(mirror
5310
5835
  .command('push')
5311
- .description('Import root markdown files into hosted Docman; use manual doc version/section/page commands for canonical graph authoring')
5836
+ .description('Import root markdown files or guarded-push one v3 source file with its adjacent sidecar')
5312
5837
  .option('--source-dir <path>', 'Directory containing root *.md files', 'docs')
5838
+ .option('--source-file <path>', 'V3 source file to guarded-push; requires adjacent <file>.docman.json')
5839
+ .option('--sidecar <path>', 'Explicit v3 sidecar path (default: <source-file>.docman.json)')
5840
+ .option('--title <title>', 'Optional title for the new v3 source snapshot')
5841
+ .option('--publish-now', 'Atomically publish/select the pushed v3 snapshot as current')
5313
5842
  .option('--group-title <title>', 'Document group title to create/use')
5314
5843
  .option('--group-uid <uid>', 'Document group uid to create/use')
5315
5844
  .option('--document-status <value>', 'Document status: draft, published, archived', 'published')
@@ -5321,7 +5850,7 @@ export function makeDocCommand() {
5321
5850
  }));
5322
5851
  applyDocContextOptions(applyWriteOptions(mirror
5323
5852
  .command('pull')
5324
- .description('Materialize hosted Docman documents into .aops/docman read-only mirrors')
5853
+ .description('Materialize structured v2 .md mirrors and exact v3 source files with adjacent sidecars')
5325
5854
  .option('--group-uid <uid>', 'Only pull one document group uid')
5326
5855
  .option('--document-slug <slug>', 'Only pull matching document slug (repeatable)', collectRepeatedOption, [])
5327
5856
  .option('--status <value>', 'Document status filter')
@@ -5356,9 +5885,9 @@ export function makeDocCommand() {
5356
5885
  }));
5357
5886
  applyDocContextOptions(cmd
5358
5887
  .command('publish')
5359
- .description('Materialize saved document content to markdown or html')
5888
+ .description('Materialize saved document content to markdown, html, or exact source')
5360
5889
  .requiredOption('--document-version-id <id>', 'Document version id')
5361
- .option('--target <target>', 'Publish target: markdown or html', 'markdown')
5890
+ .option('--target <target>', 'Publish target: markdown, html, or source', 'markdown')
5362
5891
  .option('--out <path>', 'Write materialized content to a file path')
5363
5892
  .option('--section-id <id>', 'Section id')
5364
5893
  .option('--page-version-id <id>', 'Page version id')