@ankhorage/devtools 1.14.8 → 1.15.1

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 (51) hide show
  1. package/README.md +114 -1
  2. package/dist/apmRelease.d.ts +4 -0
  3. package/dist/apmRelease.js +3 -0
  4. package/dist/cli/bin/apm-release.d.ts +2 -0
  5. package/dist/cli/bin/apm-release.js +15 -0
  6. package/dist/cli/commands/apm/sync.d.ts +5 -0
  7. package/dist/cli/commands/apm/sync.js +5 -0
  8. package/dist/cli/commands/apm/validate.d.ts +5 -0
  9. package/dist/cli/commands/apm/validate.js +5 -0
  10. package/dist/cli/commands.d.ts +6 -2
  11. package/dist/cli/commands.js +15 -1
  12. package/dist/cli/index.d.ts +2 -2
  13. package/dist/cli/index.js +9 -0
  14. package/dist/cli/runProviderCommand.js +5 -0
  15. package/dist/cli/runRepositoryCommand.js +2 -1
  16. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.d.ts +10 -0
  17. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js +51 -0
  18. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.d.ts +3 -0
  19. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.js +31 -0
  20. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.d.ts +3 -0
  21. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.js +84 -0
  22. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.d.ts +11 -0
  23. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.js +82 -0
  24. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.d.ts +2 -0
  25. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.js +9 -0
  26. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.d.ts +2 -0
  27. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js +18 -0
  28. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.d.ts +3 -0
  29. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.js +29 -0
  30. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.d.ts +11 -0
  31. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.js +76 -0
  32. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.d.ts +2 -0
  33. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.js +6 -0
  34. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.d.ts +12 -0
  35. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.js +14 -0
  36. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.d.ts +2 -0
  37. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.js +6 -0
  38. package/dist/tools/agents/index.js +58 -18
  39. package/dist/tools/gitignore/index.d.ts +10 -0
  40. package/dist/tools/gitignore/index.js +43 -0
  41. package/dist/tools/skills/assets/ankhorage-coding-rules/SKILL.md +28 -11
  42. package/dist/tools/workflows/files/ci.yml +7 -0
  43. package/dist/tools/workflows/files/release.yml +35 -1
  44. package/dist/tools/workflows/index.js +4 -1
  45. package/dist/tools/workflows/renderWorkflowAsync.d.ts +1 -0
  46. package/dist/tools/workflows/renderWorkflowAsync.js +1 -0
  47. package/dist/types/apm-release-validation.d.ts +33 -0
  48. package/dist/types/apm-release-validation.js +1 -0
  49. package/dist/types/public.d.ts +1 -0
  50. package/dist/types/public.js +1 -0
  51. package/package.json +17 -4
@@ -0,0 +1,18 @@
1
+ import { resolve } from 'node:path';
2
+ import { readApmReleaseManifestAsync } from './readApmReleaseManifestAsync.js';
3
+ /*** Use Devtools' own built CLI for self-validation and the installed dependency binary in consumers. */
4
+ export async function resolveApmReleaseCommandAsync(targetDirectory) {
5
+ try {
6
+ const manifest = await readApmReleaseManifestAsync(resolve(targetDirectory, 'package.json'));
7
+ return manifest.name === '@ankhorage/devtools'
8
+ ? 'node ./dist/cli/bin/apm-release.js'
9
+ : CONSUMER_COMMAND;
10
+ }
11
+ catch (error) {
12
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
13
+ return CONSUMER_COMMAND;
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+ const CONSUMER_COMMAND = './node_modules/.bin/ankhorage-apm-release';
@@ -0,0 +1,3 @@
1
+ import type { ApmReleaseDescriptorPort } from '../../../types/apm-release-validation.js';
2
+ /*** Bind valid owner metadata to the selected candidate version; never silently skip malformed opt-in. */
3
+ export declare function synchronizeApmReleaseMetadataAsync(port: ApmReleaseDescriptorPort): Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ import { validatePackageUpdateMetadata, validateUpdateDescriptor } from '@ankhorage/apm';
2
+ import { isRecord } from '@ankhorage/utility/object';
3
+ import { readApmReleaseMetadata } from '../utils/readApmReleaseMetadata.js';
4
+ /*** Bind valid owner metadata to the selected candidate version; never silently skip malformed opt-in. */
5
+ export async function synchronizeApmReleaseMetadataAsync(port) {
6
+ const manifest = await port.readPackageAsync();
7
+ const metadata = readApmReleaseMetadata(manifest);
8
+ if (metadata === undefined)
9
+ return false;
10
+ const discovery = validatePackageUpdateMetadata(metadata);
11
+ if (!discovery.valid || discovery.metadata === undefined)
12
+ throw new Error('Invalid APM discovery metadata.');
13
+ const descriptor = await port.readDescriptorAsync(discovery.metadata.descriptor);
14
+ const { name, version } = manifest;
15
+ if (typeof name !== 'string' ||
16
+ typeof version !== 'string' ||
17
+ !isRecord(descriptor.owner) ||
18
+ descriptor.owner.name !== name) {
19
+ throw new Error('APM release descriptor must belong to the package being versioned.');
20
+ }
21
+ const synchronized = { ...descriptor, owner: { ...descriptor.owner, version } };
22
+ const validation = validateUpdateDescriptor({
23
+ descriptor: synchronized,
24
+ expectedOwner: { name, version },
25
+ });
26
+ if (!validation.valid)
27
+ throw new Error(validation.blockers.map(({ code, reason }) => `${code}: ${reason}`).join('\n'));
28
+ return port.writeDescriptorAsync(discovery.metadata.descriptor, synchronized);
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../types/apm-release-validation.js';
2
+ /*** Validate packed owner evidence using the released APM protocol, never a copied schema.
3
+ * Opt-in packages declare ankh.apm with protocolVersion and a package-relative descriptor.
4
+ * No metadata means not applicable, not a claim that no migration is needed. The descriptor
5
+ * declares supported no-migration, required migration, or unsupported/manual history explicitly.
6
+ * Previous descriptors enforce immutable migration checksums and related descriptors validate
7
+ * cross-owner prerequisites. Package-owned source-to-target tests must also cover skipped versions,
8
+ * idempotency, interruption/recovery, and unsupported historical states.
9
+ * @readme
10
+ */
11
+ export declare function validateApmReleaseCandidate(candidate: ApmPackedReleaseCandidate, options?: ApmReleaseValidationOptions): ApmReleaseValidationResult;
@@ -0,0 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { validatePackageUpdateMetadata, validateUpdateDescriptor, validateUpdateExtensionBinding, validateUpdateExtensionCapabilities, } from '@ankhorage/apm';
3
+ import { readApmReleaseMetadata } from '../utils/readApmReleaseMetadata.js';
4
+ /*** Validate packed owner evidence using the released APM protocol, never a copied schema.
5
+ * Opt-in packages declare ankh.apm with protocolVersion and a package-relative descriptor.
6
+ * No metadata means not applicable, not a claim that no migration is needed. The descriptor
7
+ * declares supported no-migration, required migration, or unsupported/manual history explicitly.
8
+ * Previous descriptors enforce immutable migration checksums and related descriptors validate
9
+ * cross-owner prerequisites. Package-owned source-to-target tests must also cover skipped versions,
10
+ * idempotency, interruption/recovery, and unsupported historical states.
11
+ * @readme
12
+ */
13
+ export function validateApmReleaseCandidate(candidate, options = {}) {
14
+ const { name, version } = candidate.packageJson;
15
+ const identity = {
16
+ ...(typeof name === 'string' ? { packageName: name } : {}),
17
+ ...(typeof version === 'string' ? { packageVersion: version } : {}),
18
+ integrity: candidate.integrity,
19
+ };
20
+ const metadata = readApmReleaseMetadata(candidate.packageJson);
21
+ const integrityBlockers = options.expectedIntegrity !== undefined && options.expectedIntegrity !== candidate.integrity
22
+ ? ['release.integrity-mismatch: Packed bytes differ from the selected release artifact.']
23
+ : [];
24
+ if (metadata === undefined) {
25
+ return {
26
+ ...identity,
27
+ applicable: false,
28
+ valid: integrityBlockers.length === 0,
29
+ blockers: integrityBlockers,
30
+ };
31
+ }
32
+ const metadataValidation = validatePackageUpdateMetadata(metadata);
33
+ if (typeof name !== 'string' || !name || typeof version !== 'string' || !version) {
34
+ return result(identity, ['release.identity: Package must define name and exact version.']);
35
+ }
36
+ if (!metadataValidation.valid)
37
+ return result(identity, [...integrityBlockers, ...messages(metadataValidation.blockers)]);
38
+ const validation = validateUpdateDescriptor({
39
+ descriptor: candidate.descriptor,
40
+ expectedOwner: { name, version },
41
+ previousDescriptors: options.previousDescriptors,
42
+ relatedDescriptors: options.relatedDescriptors,
43
+ });
44
+ if (!validation.valid || validation.descriptor === undefined || integrityBlockers.length > 0) {
45
+ return result(identity, [...integrityBlockers, ...messages(validation.blockers)]);
46
+ }
47
+ return result(identity, extensionBlockers(candidate, validation.descriptor));
48
+ }
49
+ /*** Validate executable binding and required handlers after static metadata has passed. */
50
+ function extensionBlockers(candidate, descriptor) {
51
+ if (descriptor.extension === undefined)
52
+ return [];
53
+ const artifact = {
54
+ role: 'target',
55
+ packageName: descriptor.owner.name,
56
+ version: descriptor.owner.version,
57
+ integrity: candidate.integrity,
58
+ descriptorDigest: createHash('sha256').update(candidate.descriptorSource).digest('hex'),
59
+ };
60
+ if (!isBoundExtension(artifact, candidate.extension)) {
61
+ return messages(validateUpdateExtensionBinding(artifact, candidate.extension));
62
+ }
63
+ return messages(validateUpdateExtensionCapabilities(descriptor, artifact, candidate.extension));
64
+ }
65
+ /*** Narrow unknown executable evidence only after the canonical runtime validator accepts it. */
66
+ function isBoundExtension(artifact, value) {
67
+ return validateUpdateExtensionBinding(artifact, value).length === 0;
68
+ }
69
+ /*** Preserve structured candidate identity and canonical blocker reasons. */
70
+ function result(identity, blockers) {
71
+ return { ...identity, applicable: true, valid: blockers.length === 0, blockers };
72
+ }
73
+ /*** Render the protocol's own diagnostics without reinterpreting their semantics. */
74
+ function messages(blockers) {
75
+ return blockers.map(({ code, reason }) => `${code}: ${reason}`);
76
+ }
@@ -0,0 +1,2 @@
1
+ /*** Compose descriptor version synchronization with the bounded Node filesystem adapter. */
2
+ export declare function synchronizeApmReleaseDescriptorAsync(targetDirectory: string): Promise<boolean>;
@@ -0,0 +1,6 @@
1
+ import { createApmReleaseDescriptorPort } from '../adapters/outbound/createApmReleaseDescriptorPort.js';
2
+ import { synchronizeApmReleaseMetadataAsync } from '../application/synchronizeApmReleaseMetadataAsync.js';
3
+ /*** Compose descriptor version synchronization with the bounded Node filesystem adapter. */
4
+ export async function synchronizeApmReleaseDescriptorAsync(targetDirectory) {
5
+ return synchronizeApmReleaseMetadataAsync(createApmReleaseDescriptorPort(targetDirectory));
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../types/apm-release-validation.js';
2
+ /*** Validate script-free publish bytes and optionally retain that exact accepted archive.
3
+ * Release order is APM protocol, package-owner metadata/code, then consuming applications.
4
+ * Build after Changesets versioning and descriptor synchronization. Execute package-owned
5
+ * acceptance tests, then validate with explicit owner-code consent. The child probe uses the
6
+ * packed package's native public export resolution and installed dependencies, not omitted source.
7
+ * Consent permits trusted executable code; a child process is not a security sandbox.
8
+ * An artifactPath retains accepted bytes exclusively, so publication need not repack or rerun
9
+ * lifecycle scripts. expectedIntegrity rejects a stale candidate before any owner code loads.
10
+ * @readme
11
+ */
12
+ export declare function validatePackedApmReleaseAsync(targetDirectory: string, options?: ApmReleaseValidationOptions): Promise<ApmReleaseValidationResult>;
@@ -0,0 +1,14 @@
1
+ import { inspectPackedApmReleaseCandidateAsync } from '../adapters/outbound/inspectPackedApmReleaseCandidateAsync.js';
2
+ /*** Validate script-free publish bytes and optionally retain that exact accepted archive.
3
+ * Release order is APM protocol, package-owner metadata/code, then consuming applications.
4
+ * Build after Changesets versioning and descriptor synchronization. Execute package-owned
5
+ * acceptance tests, then validate with explicit owner-code consent. The child probe uses the
6
+ * packed package's native public export resolution and installed dependencies, not omitted source.
7
+ * Consent permits trusted executable code; a child process is not a security sandbox.
8
+ * An artifactPath retains accepted bytes exclusively, so publication need not repack or rerun
9
+ * lifecycle scripts. expectedIntegrity rejects a stale candidate before any owner code loads.
10
+ * @readme
11
+ */
12
+ export async function validatePackedApmReleaseAsync(targetDirectory, options = {}) {
13
+ return inspectPackedApmReleaseCandidateAsync(targetDirectory, options);
14
+ }
@@ -0,0 +1,2 @@
1
+ /*** Read opt-in owner metadata without copying the APM discovery schema. */
2
+ export declare function readApmReleaseMetadata(manifest: Readonly<Record<string, unknown>>): unknown;
@@ -0,0 +1,6 @@
1
+ import { isRecord, readOwnProperty } from '@ankhorage/utility/object';
2
+ /*** Read opt-in owner metadata without copying the APM discovery schema. */
3
+ export function readApmReleaseMetadata(manifest) {
4
+ const ankh = readOwnProperty(manifest, 'ankh');
5
+ return isRecord(ankh) ? readOwnProperty(ankh, 'apm') : undefined;
6
+ }
@@ -11,6 +11,13 @@ async function renderAgentsFile(targetDirectory) {
11
11
  const manifest = await readPackageManifest(targetDirectory);
12
12
  const packageName = readNonEmptyString(manifest.name) ?? 'Package name not declared';
13
13
  const description = readNonEmptyString(manifest.description) ?? 'No package description is declared.';
14
+ const sections = [
15
+ renderCurrentArchitectureInstructions(),
16
+ renderRequiredRepositoryInstructions(),
17
+ renderDocumentationInstructions(),
18
+ renderPullRequestInstructions(),
19
+ renderSkillScriptInstructions(),
20
+ ].join('');
14
21
  return `# AGENTS.md
15
22
 
16
23
  <!-- This file is managed by @ankhorage/devtools. -->
@@ -21,7 +28,11 @@ Package: \`${escapeInlineCode(packageName)}\`
21
28
 
22
29
  ${description}
23
30
 
24
- ## Current architecture only
31
+ ${sections}`;
32
+ }
33
+ /*** Render the current-architecture policy shared by every managed repository. */
34
+ function renderCurrentArchitectureInstructions() {
35
+ return `## Current architecture only
25
36
 
26
37
  Only the current Ankhorage architecture is valid. Do not add or retain deprecated APIs,
27
38
  compatibility aliases, shims, dual old/new paths, historical-state fallbacks, or migrations whose
@@ -34,27 +45,47 @@ published public APIs and declared dependencies, never sibling source files.
34
45
  Current-runtime error handling and canonical database or infrastructure migrations remain valid
35
46
  when they support states that the current architecture can intentionally produce.
36
47
 
37
- ## Required repository instructions
48
+ `;
49
+ }
50
+ /*** Render contextual repository skill-selection requirements. */
51
+ function renderRequiredRepositoryInstructions() {
52
+ return `## Required repository instructions
38
53
 
39
54
  Before changing any file, read this \`AGENTS.md\` completely and inspect \`.agents/skills/\`.
40
- For every change, load and follow both baseline repository skills before making changes:
55
+ Treat skill selection as a mandatory precondition to editing, then follow every selected skill
56
+ through validation and delivery.
57
+
58
+ - Load \`.agents/skills/ankhorage-coding-rules/SKILL.md\` for implementation, refactoring, testing,
59
+ review, or pull-request delivery work.
60
+ - Load \`.agents/skills/ankhorage-project-structure/SKILL.md\` when the task changes or reviews
61
+ directory ownership, package boundaries, public entrypoints, cross-repository ownership, type or
62
+ utility placement, or source architecture.
63
+ - Load every additional repository-local skill whose description or requirements match the task,
64
+ including skills required by any selected skill. These rules define mandatory minimums, not an
65
+ allow-list; do not skip a useful relevant skill merely because it is not named here.
66
+
67
+ Do not load unrelated skills merely because they are installed. If the task scope expands, inspect
68
+ \`.agents/skills/\` again and load the newly relevant skills before continuing. Do not substitute
69
+ remembered, globally installed, or generic guidance for the repository-local versions.
41
70
 
42
- 1. \`.agents/skills/ankhorage-coding-rules/SKILL.md\`
43
- 2. \`.agents/skills/ankhorage-project-structure/SKILL.md\`
44
-
45
- Load and follow every additional repository-local skill relevant to the task, including skills
46
- required by either baseline skill. Continue to follow these instructions and skills through
47
- validation and delivery; do not substitute remembered, globally installed, or generic guidance for
48
- the repository-local versions.
49
-
50
- ## Documentation
71
+ `;
72
+ }
73
+ /*** Render the release-owned generated documentation policy. */
74
+ function renderDocumentationInstructions() {
75
+ return `## Documentation
51
76
 
52
- \`README.md\` and the \`paradox/\` documentation tree are generated by \`@ankhorage/paradox\`.
53
- Never edit generated documentation directly. Update the owning Paradox \`/*** ... */\` comments in
54
- \`src\`, including \`@readme\` tags where the content belongs in the README, then run \`bun docs\`
55
- and commit the regenerated artifacts.
77
+ \`README.md\` and the configured Paradox output are generated release artifacts. Never edit them
78
+ manually, and do not regenerate or commit them in ordinary feature pull requests. Update the owning
79
+ Paradox \`/*** ... */\` comments in \`src\` and leading \`@usage\` comments in real
80
+ \`examples/<example>/...\` source files, plus any repository-owned manual documentation outside the
81
+ generated output. The managed release workflow runs \`bun run docs\` after the package version bump
82
+ and commits the regenerated artifacts in the release commit.
56
83
 
57
- ## Pull requests
84
+ `;
85
+ }
86
+ /*** Render canonical pull-request validation commands. */
87
+ function renderPullRequestInstructions() {
88
+ return `## Pull requests
58
89
 
59
90
  Before creating a pull request, run all of these commands in this order and resolve every failure:
60
91
 
@@ -67,13 +98,18 @@ bun run changeset
67
98
  bun run format
68
99
  \`\`\`
69
100
 
70
- ## Skill scripts
101
+ `;
102
+ }
103
+ /*** Render the repository rule for executable Agent Skill scripts. */
104
+ function renderSkillScriptInstructions() {
105
+ return `## Skill scripts
71
106
 
72
107
  Scripts inside an Agent Skill must always be TypeScript files with the \`.ts\` extension.
73
108
  JavaScript skill scripts using \`.js\`, \`.mjs\`, or \`.cjs\` are not allowed. Run TypeScript
74
109
  skill scripts with Bun.
75
110
  `;
76
111
  }
112
+ /*** Read the target package manifest used to personalize managed repository instructions. */
77
113
  async function readPackageManifest(targetDirectory) {
78
114
  try {
79
115
  const contents = await readFile(resolve(targetDirectory, 'package.json'), 'utf8');
@@ -90,15 +126,19 @@ async function readPackageManifest(targetDirectory) {
90
126
  throw error;
91
127
  }
92
128
  }
129
+ /*** Escape inline-code delimiters before interpolating repository metadata into Markdown. */
93
130
  function escapeInlineCode(value) {
94
131
  return value.replaceAll('`', '\\`');
95
132
  }
133
+ /*** Check whether an unknown failure carries a Node.js error code. */
96
134
  function isNodeError(error) {
97
135
  return error instanceof Error && 'code' in error;
98
136
  }
137
+ /*** Check whether an unknown value is a plain record-shaped object. */
99
138
  function isRecord(value) {
100
139
  return typeof value === 'object' && value !== null && !Array.isArray(value);
101
140
  }
141
+ /*** Read and trim a non-empty string value from package metadata. */
102
142
  function readNonEmptyString(value) {
103
143
  return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
104
144
  }
@@ -0,0 +1,10 @@
1
+ export declare const gitignoreManagedFiles: readonly [{
2
+ readonly relativePath: ".gitignore";
3
+ readonly render: typeof renderGitignore;
4
+ readonly isApplicable: typeof isGitignorePresent;
5
+ }];
6
+ /*** Preserve repository-owned ignore rules while keeping documentation directories trackable. */
7
+ declare function renderGitignore(targetDirectory: string): Promise<string>;
8
+ /*** Report whether a repository already owns a `.gitignore` file that can be normalized. */
9
+ declare function isGitignorePresent(targetDirectory: string): Promise<boolean>;
10
+ export {};
@@ -0,0 +1,43 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ const TRACKED_DOCUMENTATION_PATTERNS = new Set([
4
+ 'docs',
5
+ 'docs/',
6
+ '/docs',
7
+ '/docs/',
8
+ 'paradox',
9
+ 'paradox/',
10
+ '/paradox',
11
+ '/paradox/',
12
+ ]);
13
+ export const gitignoreManagedFiles = [
14
+ {
15
+ relativePath: '.gitignore',
16
+ render: renderGitignore,
17
+ isApplicable: isGitignorePresent,
18
+ },
19
+ ];
20
+ /*** Preserve repository-owned ignore rules while keeping documentation directories trackable. */
21
+ async function renderGitignore(targetDirectory) {
22
+ const contents = await readFile(resolve(targetDirectory, '.gitignore'), 'utf8');
23
+ return contents
24
+ .split('\n')
25
+ .filter((line) => !TRACKED_DOCUMENTATION_PATTERNS.has(line.trim()))
26
+ .join('\n');
27
+ }
28
+ /*** Report whether a repository already owns a `.gitignore` file that can be normalized. */
29
+ async function isGitignorePresent(targetDirectory) {
30
+ try {
31
+ await access(resolve(targetDirectory, '.gitignore'));
32
+ return true;
33
+ }
34
+ catch (error) {
35
+ if (isNodeError(error) && error.code === 'ENOENT')
36
+ return false;
37
+ throw error;
38
+ }
39
+ }
40
+ /*** Check whether an unknown failure carries a Node.js error code. */
41
+ function isNodeError(error) {
42
+ return error instanceof Error && 'code' in error;
43
+ }
@@ -45,6 +45,31 @@ exceptions or replaced by generic preferences from this skill.
45
45
  - Keep one canonical implementation. Do not add compatibility aliases, dual paths, historical-state
46
46
  fallbacks, or sibling-source imports.
47
47
 
48
+ ## Comments and Paradox documentation
49
+
50
+ - Give every named production function under `src` a concise Paradox `/*** ... */` comment,
51
+ including exported functions and internal named helpers. Inline callbacks are exempt.
52
+ - Use ordinary inline comments when they explain non-obvious intent, invariants, constraints, or the
53
+ reason behind a decision. Do not narrate self-explanatory code or duplicate what names and types
54
+ already express.
55
+ - Paradox documentation metadata belongs only inside `/*** ... */` comments. Use only supported
56
+ Paradox tags: `@readme`, `@config`, `@example`, and `@usage`. Do not use JSDoc-only tags such as
57
+ `@param` or `@returns` as Paradox metadata.
58
+ - Add a supported Paradox tag only when it changes or usefully enriches generated documentation;
59
+ plain function descriptions do not need tags.
60
+ - README usage documentation must come from a real repository-root `examples/<example>/...` source
61
+ file. Put `@usage` in that example file's leading `/*** ... */` comment so Paradox promotes the
62
+ runnable example into the README Usage section. Do not create dedicated `readme-usage`,
63
+ `usage-readme`, `readmeUsage`, or equivalent source modules whose only purpose is feeding README
64
+ usage text.
65
+ - Update documentation sources in the pull request, including repository-owned manual documentation
66
+ outside the configured generated output. Never hand-edit generated README or Paradox artifacts.
67
+ - Generated documentation is release-owned. Ordinary feature pull requests must not regenerate or
68
+ commit `README.md` or the configured Paradox output. The managed release workflow runs
69
+ `bun run docs` after the package version bump and commits the result in the release commit.
70
+ - Run `bun run docs` locally only when the task explicitly changes or validates Paradox generation;
71
+ do not commit that generated diff from an ordinary feature pull request.
72
+
48
73
  ## Testing
49
74
 
50
75
  - Test observable behavior at the owning boundary. Keep tests deterministic and independent of
@@ -60,15 +85,6 @@ exceptions or replaced by generic preferences from this skill.
60
85
  full reruns without a substantive code or configuration change, stop cycling, inspect logs and
61
86
  artifacts, and report the blocker.
62
87
 
63
- ## Paradox documentation
64
-
65
- - Give every named production function under `src` a concise Paradox `/*** ... */` description,
66
- including internal helpers. Inline callbacks are exempt.
67
- - Add `@param`, `@returns`, `@example`, or `@readme` to public APIs only when the tag adds useful
68
- information.
69
- - Update documentation sources and regenerate tracked Paradox output. Never hand-edit generated
70
- README or Paradox artifacts.
71
-
72
88
  ## Public API and release quality
73
89
 
74
90
  - Export public symbols intentionally from canonical entrypoints and exercise those exports in
@@ -77,7 +93,8 @@ exceptions or replaced by generic preferences from this skill.
77
93
  public API changes.
78
94
  - Justify new dependencies and keep generated artifacts, lockfiles, and package metadata aligned
79
95
  with the repository's owning workflows.
80
- - Review the final diff, then run the applicable build, lint, test, typecheck, Knip, format, docs,
81
- changeset-status, and packaging checks.
96
+ - Review the final diff, then run the applicable build, lint, test, typecheck, Knip, format,
97
+ changeset-status, and packaging checks. Run documentation generation only when explicitly needed
98
+ to validate documentation tooling behavior.
82
99
  - Report the exact commands run and any omissions or failures. Do not imply a fully green handoff
83
100
  when a required check did not run or pass.
@@ -41,6 +41,13 @@ jobs:
41
41
  echo "No build script found; skipping."
42
42
  fi
43
43
 
44
+ - name: Validate opted-in APM owner release
45
+ run: |
46
+ if node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
47
+ bun run test:apm
48
+ __ANKH_APM_RELEASE_COMMAND__ validate . --allow-owner-code
49
+ fi
50
+
44
51
  - name: Validate Ankhorage repository
45
52
  run: bunx @ankhorage/ankh doctor validate .
46
53
 
@@ -73,8 +73,19 @@ jobs:
73
73
 
74
74
  before_version="$(node -p "require('./package.json').version")"
75
75
  __ANKH_CHANGESETS_VERSION_COMMAND__
76
+ if node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
77
+ __ANKH_APM_RELEASE_COMMAND__ sync .
78
+ fi
76
79
  after_version="$(node -p "require('./package.json').version")"
77
80
 
81
+ if [ "$before_version" != "$after_version" ]; then
82
+ if node -e "const p=require('./package.json'); process.exit(p.scripts?.docs ? 0 : 1)"; then
83
+ bun run docs
84
+ else
85
+ echo "No docs script found; skipping."
86
+ fi
87
+ fi
88
+
78
89
  if git diff --quiet; then
79
90
  echo "versioned=false" >> "$GITHUB_OUTPUT"
80
91
  exit 0
@@ -100,7 +111,30 @@ jobs:
100
111
 
101
112
  - name: Publish unpublished packages
102
113
  if: hashFiles('.changeset/config.json') != ''
103
- run: __ANKH_CHANGESETS_PUBLISH_COMMAND__
114
+ run: |
115
+ if ! node -e "const p=require('./package.json'); process.exit(p.ankh?.apm === undefined ? 1 : 0)"; then
116
+ __ANKH_CHANGESETS_PUBLISH_COMMAND__
117
+ exit 0
118
+ fi
119
+
120
+ package_name="$(node -p "require('./package.json').name")"
121
+ package_version="$(node -p "require('./package.json').version")"
122
+ registry_evidence="${RUNNER_TEMP}/apm-registry.json"
123
+ if npm view "${package_name}@${package_version}" version --json > "$registry_evidence"; then
124
+ echo "Exact APM owner version is already published."
125
+ else
126
+ # Authentication/network errors must never be treated as an unpublished version.
127
+ node -e "const p=require(process.argv[1]); if(p.error?.code !== 'E404') process.exit(1)" "$registry_evidence"
128
+ bun install --frozen-lockfile --ignore-scripts
129
+ bun run build
130
+ bun run test:apm
131
+ artifact="$(mktemp -d "${RUNNER_TEMP}/apm-publish.XXXXXX")/candidate.tgz"
132
+ __ANKH_APM_RELEASE_COMMAND__ validate . --allow-owner-code --artifact "$artifact"
133
+ npm publish "$artifact" --ignore-scripts
134
+ fi
135
+ if ! git rev-parse -q --verify "refs/tags/v${package_version}" >/dev/null; then
136
+ git tag "v${package_version}"
137
+ fi
104
138
 
105
139
  - name: Finalize Changesets v3 tags and GitHub releases
106
140
  if: hashFiles('.changeset/config.json') != ''
@@ -1,5 +1,6 @@
1
1
  import { access } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { resolveApmReleaseCommandAsync } from '../../features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js';
3
4
  import { bunRuntimePolicy, nodeRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
4
5
  import { renderRenovateWorkflowAsync } from './renderRenovateWorkflowAsync.js';
5
6
  import { renderWorkflowAsync } from './renderWorkflowAsync.js';
@@ -30,7 +31,8 @@ function createWorkflowDefinition(relativePath, sourcePath) {
30
31
  const sourceUrl = new URL(sourcePath, import.meta.url);
31
32
  return {
32
33
  relativePath,
33
- render: async () => await renderWorkflowAsync(sourceUrl, {
34
+ render: async (targetDirectory) => await renderWorkflowAsync(sourceUrl, {
35
+ apmReleaseCommand: await resolveApmReleaseCommandAsync(targetDirectory),
34
36
  bunVersion: bunRuntimePolicy.version,
35
37
  nodeVersion: nodeRuntimePolicy.setupVersion,
36
38
  }),
@@ -42,6 +44,7 @@ function createRenovateWorkflowDefinition() {
42
44
  return {
43
45
  relativePath: '.github/workflows/renovate.yml',
44
46
  render: async (targetDirectory) => await renderRenovateWorkflowAsync(sourceUrl, targetDirectory, {
47
+ apmReleaseCommand: await resolveApmReleaseCommandAsync(targetDirectory),
45
48
  bunVersion: bunRuntimePolicy.version,
46
49
  nodeVersion: nodeRuntimePolicy.setupVersion,
47
50
  }),
@@ -1,6 +1,7 @@
1
1
  export interface WorkflowPolicy {
2
2
  readonly bunVersion: string;
3
3
  readonly nodeVersion: string;
4
+ readonly apmReleaseCommand?: string;
4
5
  }
5
6
  /*** Renders one workflow template with the canonical runtime and Changesets policy. */
6
7
  export declare function renderWorkflowAsync(sourceUrl: URL, policy: WorkflowPolicy): Promise<string>;
@@ -8,6 +8,7 @@ export async function renderWorkflowAsync(sourceUrl, policy) {
8
8
  .replaceAll(CHANGESETS_PUBLISH_COMMAND_TOKEN, changesetsPolicy.workflowCommands.publish)
9
9
  .replaceAll(CHANGESETS_STATUS_COMMAND_TOKEN, changesetsPolicy.workflowCommands.status)
10
10
  .replaceAll(CHANGESETS_VERSION_COMMAND_TOKEN, changesetsPolicy.workflowCommands.version)
11
+ .replaceAll('__ANKH_APM_RELEASE_COMMAND__', policy.apmReleaseCommand ?? './node_modules/.bin/ankhorage-apm-release')
11
12
  .replaceAll(NODE_VERSION_TOKEN, policy.nodeVersion);
12
13
  }
13
14
  const BUN_VERSION_TOKEN = '__ANKH_BUN_VERSION__';
@@ -0,0 +1,33 @@
1
+ import type { ApmUpdateDescriptor } from '@ankhorage/apm/types';
2
+ /*** Packed bytes and loaded owner evidence supplied to the canonical APM validators. */
3
+ export interface ApmPackedReleaseCandidate {
4
+ readonly packageJson: Readonly<Record<string, unknown>>;
5
+ readonly descriptor: unknown;
6
+ readonly descriptorSource: string;
7
+ readonly integrity: string;
8
+ readonly extension?: unknown;
9
+ }
10
+ /*** Explicit release-validation inputs; owner code is never trusted by default. */
11
+ export interface ApmReleaseValidationOptions {
12
+ readonly allowOwnerCode?: boolean;
13
+ readonly artifactPath?: string;
14
+ readonly expectedIntegrity?: string;
15
+ readonly previousDescriptors?: readonly ApmUpdateDescriptor[];
16
+ readonly relatedDescriptors?: readonly ApmUpdateDescriptor[];
17
+ }
18
+ /*** Evidence for one exact packed release; inapplicable never means migration-safe. */
19
+ export interface ApmReleaseValidationResult {
20
+ readonly applicable: boolean;
21
+ readonly valid: boolean;
22
+ readonly packageName?: string;
23
+ readonly packageVersion?: string;
24
+ readonly integrity?: string;
25
+ readonly artifactPath?: string;
26
+ readonly blockers: readonly string[];
27
+ }
28
+ /*** Filesystem capabilities required by descriptor synchronization, independent of transport. */
29
+ export interface ApmReleaseDescriptorPort {
30
+ readonly readPackageAsync: () => Promise<Readonly<Record<string, unknown>>>;
31
+ readonly readDescriptorAsync: (subpath: string) => Promise<Readonly<Record<string, unknown>>>;
32
+ readonly writeDescriptorAsync: (subpath: string, descriptor: Readonly<Record<string, unknown>>) => Promise<boolean>;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult, } from './apm-release-validation.js';
@@ -0,0 +1 @@
1
+ export {};