@aviaratech/ai-delivery 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -323,7 +323,7 @@ The classifier owns the digest values and must include every relevant source, co
323
323
 
324
324
  Verification holds one file lease per worktree and durably records its writer and observed command process identities. A second writer in the same worktree is rejected before execution. Separate worktrees can overlap, including at the same head, with separate stage inputs and mutable manifests. Callers must first qualify their commands' CPU, memory, disk, I/O and shared-file/service requirements; a resource class alone grants no shared-resource exclusivity. The runtime adds no global executor cap. On retry after writer interruption, it refuses live or unknown ownership and reconciles only the recorded command group and observed descendants before reusing compatible completed stages. A launch interrupted before command identity was recorded requires explicit reconciliation. Process identity observation is required on supported POSIX hosts; unavailable or ambiguous observation fails closed.
325
325
 
326
- The `--admit` flag explicitly admits additional resource classes. Publication creates a draft PR only after exact-head verification; a high-risk policy also requires a prepublication review artifact. Ready promotion and merge require a submitted formal review. Merge checks live blockers, checks, base/head coordinates, and the policy boundary. Repositories may require an exact base/head lease for merge.
326
+ The `--admit` flag explicitly admits additional resource classes for execution. Compatible complete stages are reused only after their existing stage, artifact, command-output and applicable resource proofs are fully validated, without admitting execution of those classes. Missing or incompatible proof requires the current invocation's admission for the actual stage class before any command runs; corrupt proof remains a hard failure. This permits serial `source_only`, `model`, then `postgres_docker` invocations to reuse prior completed stages while admitting only the next class to execute. Publication creates a draft PR only after exact-head verification; a high-risk policy also requires a prepublication review artifact. Ready promotion and merge require a submitted formal review. Merge checks live blockers, checks, base/head coordinates, and the policy boundary. Repositories may require an exact base/head lease for merge.
327
327
 
328
328
  For a bounded run, pass `--max-aggregate-rss-bytes`, `--min-free-disk-bytes`, and optionally `--max-new-output-bytes` with one or more `--output-root` paths to `verify`. `issue_verify` accepts the same values in `resourceBounds`. RSS is sampled across the observed child process tree, including observed detached descendants; free disk is checked before commands and during execution on the worktree and declared output filesystems. Positive file-size growth is sampled under the declared roots, with previously measured completed stages carried across a resume. The caller must declare every output root relevant to its allowance; aliases must resolve within those roots. The roots are relative to the issue worktree unless absolute. Output growth is separate from the 8 MiB captured stdout/stderr limit. Resource limits are opt in and do not impose a total runtime deadline.
329
329
 
@@ -12,7 +12,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
12
12
  import { AI_DELIVERY_MCP_TOOLS } from './mcp/tools.js';
13
13
  import { createAiDeliveryMcpServer } from './mcp/index.js';
14
14
  import { loadDeliveryConfig, parseDeliveryConfig } from './config/deliveryConfig.js';
15
- import { digestValue } from './delivery/index.js';
15
+ import { digestValue, stableJson } from './delivery/index.js';
16
16
  import { contextFor, executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
17
17
  import { defaultBaseRef, gitCommonDir } from './git.js';
18
18
  import * as githubClient from './github/client.js';
@@ -210,6 +210,7 @@ async function fixture(options = {}) {
210
210
  const remoteName = options.remote ?? 'origin';
211
211
  const counter = join(root, '.git', 'stage-count.txt');
212
212
  const secondCounter = join(root, '.git', 'second-stage-count.txt');
213
+ const thirdCounter = join(root, '.git', 'third-stage-count.txt');
213
214
  const failSecond = join(root, '.git', 'fail-second-stage');
214
215
  try {
215
216
  git(root, 'init', '-q', '-b', 'main');
@@ -290,11 +291,16 @@ export default {
290
291
  requiredStages: [{ id: 'check', dependsOn: [], semanticInputKeys: ['source'],
291
292
  ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./artifact.txt', import.meta.url))) }]," : ''}
292
293
  resourceClass: 'source_only', commands: [{ label: 'count', argv: [process.execPath, '-e',
293
- ${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages
294
+ ${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages || options.serialResourceStages
294
295
  ? `, { id: 'second', dependsOn: ['check'], semanticInputKeys: ['source'],
295
296
  ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./component.txt', import.meta.url))) }]," : ''}
296
- resourceClass: 'source_only', commands: [{ label: 'retry', argv: [process.execPath, '-e',
297
+ resourceClass: '${options.serialResourceStages ? 'model' : 'source_only'}', commands: [{ label: 'retry', argv: [process.execPath, '-e',
297
298
  ${JSON.stringify(options.secondStageScript ?? `const fs=require('fs');const p=${JSON.stringify(secondCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));if(fs.existsSync(${JSON.stringify(failSecond)}))process.exit(7);`)}] }] }`
299
+ : ''}${options.serialResourceStages
300
+ ? `, { id: 'postgres', dependsOn: ['second'], semanticInputKeys: ['source'],
301
+ ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./component.txt', import.meta.url))) }]," : ''}
302
+ resourceClass: 'postgres_docker', commands: [{ label: 'count', argv: [process.execPath, '-e',
303
+ ${JSON.stringify(`const fs=require('fs');const p=${JSON.stringify(thirdCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }`
298
304
  : ''}],
299
305
  risk: 'standard' };
300
306
  },
@@ -353,7 +359,7 @@ export default {
353
359
  mkdirSync(dirname(admissionPath), { recursive: true });
354
360
  if (!options.omitRuntimeAdmission)
355
361
  writeFileSync(admissionPath, JSON.stringify({ ...content, admissionId: digestValue(content) }), { mode: 0o600 });
356
- return { root, counter, secondCounter, failSecond, runtimeEntryPath };
362
+ return { root, counter, secondCounter, thirdCounter, failSecond, runtimeEntryPath };
357
363
  }
358
364
  catch (error) {
359
365
  rmSync(root, { recursive: true, force: true });
@@ -1803,6 +1809,224 @@ test('a bounded passing run records measured resources in versioned evidence', a
1803
1809
  rmSync(root, { recursive: true, force: true });
1804
1810
  }
1805
1811
  });
1812
+ test.each([
1813
+ { bounded: false, componentPolicy: false },
1814
+ { bounded: true, componentPolicy: false },
1815
+ { bounded: true, componentPolicy: true },
1816
+ ])('serial admission reuses complete stages with bounds $bounded and component policy $componentPolicy', async ({ bounded, componentPolicy }) => {
1817
+ const { root, counter, secondCounter, thirdCounter } = await fixture({
1818
+ serialResourceStages: true,
1819
+ componentPolicy,
1820
+ });
1821
+ try {
1822
+ const row = await prepareIssueWorktree({
1823
+ baseRef: 'main',
1824
+ identity: 'synthetic-author',
1825
+ issueNumber: 17,
1826
+ repoRoot: root,
1827
+ });
1828
+ const input = {
1829
+ issueNumber: 17,
1830
+ repoRoot: row.path,
1831
+ ...(bounded ? { resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 } } : {}),
1832
+ };
1833
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['source_only'] }), /Stage 'second' requires explicit model admission/u);
1834
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1835
+ assert.equal(existsSync(secondCounter), false);
1836
+ assert.equal(existsSync(thirdCounter), false);
1837
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['model'] }), /Stage 'postgres' requires explicit postgres_docker admission/u);
1838
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1839
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
1840
+ assert.equal(existsSync(thirdCounter), false);
1841
+ const completed = await verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] });
1842
+ const repeated = await verifyIssue({ ...input, admittedResourceClasses: [] });
1843
+ assert.deepEqual(repeated.stageReceipts, completed.stageReceipts);
1844
+ for (const path of [counter, secondCounter, thirdCounter])
1845
+ assert.equal(readFileSync(path, 'utf8'), '1');
1846
+ if (bounded) {
1847
+ assert.deepEqual(repeated.resources, completed.resources);
1848
+ assert.ok((repeated.resources?.sampleCount ?? 0) > 0);
1849
+ const first = completed.stageReceipts[0];
1850
+ const stagePath = join(root, '.git', 'ai-delivery', first.input.schemaVersion === 'ai-delivery.stage-input@2' ? 'verification@2' : 'verification@1', 'stages', 'check', `${first.input.inputId.slice(7)}.json`);
1851
+ rmSync(stagePath);
1852
+ const repaired = await verifyIssue({ ...input, admittedResourceClasses: [] });
1853
+ assert.deepEqual(repaired.stageReceipts, completed.stageReceipts);
1854
+ assert.deepEqual(repaired.resources, completed.resources);
1855
+ assert.equal(existsSync(stagePath), true);
1856
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1857
+ }
1858
+ }
1859
+ finally {
1860
+ rmSync(root, { recursive: true, force: true });
1861
+ }
1862
+ });
1863
+ test.each(['missing', 'unmeasured', 'source', 'command', 'environment', 'runtime', 'producer', 'bounds'])('serial admission refuses execution after %s proof invalidation', async (changed) => {
1864
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
1865
+ const previousPath = process.env.PATH;
1866
+ const versionDescriptor = Object.getOwnPropertyDescriptor(process, 'version');
1867
+ const producerPath = join(import.meta.dirname, 'serial-producer-fixture.js');
1868
+ let producerOwned = false;
1869
+ try {
1870
+ const row = await prepareIssueWorktree({
1871
+ baseRef: 'main',
1872
+ identity: 'synthetic-author',
1873
+ issueNumber: 17,
1874
+ repoRoot: root,
1875
+ });
1876
+ const input = {
1877
+ issueNumber: 17,
1878
+ repoRoot: row.path,
1879
+ resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
1880
+ };
1881
+ const completed = await verifyIssue({
1882
+ ...(changed === 'unmeasured' ? { issueNumber: input.issueNumber, repoRoot: input.repoRoot } : input),
1883
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
1884
+ });
1885
+ if (changed === 'missing') {
1886
+ const id = completed.stageReceipts[0].input.inputId.slice(7);
1887
+ rmSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check', `${id}.json`));
1888
+ rmSync(join(root, '.git', 'ai-delivery', 'resource-stages@1', `${id}.json`));
1889
+ }
1890
+ else if (changed === 'source' || changed === 'command') {
1891
+ const path = join(row.path, changed === 'source' ? 'change.txt' : 'policy.mjs');
1892
+ writeFileSync(path, changed === 'source'
1893
+ ? 'changed source\n'
1894
+ : readFileSync(path, 'utf8').replace("label: 'count'", "label: 'changed-count'"));
1895
+ git(row.path, 'add', '.');
1896
+ git(row.path, 'commit', '-qm', 'synthetic checkpoint invalidation');
1897
+ }
1898
+ else if (changed === 'environment')
1899
+ process.env.PATH = `${previousPath ?? ''}:/synthetic-environment`;
1900
+ else if (changed === 'runtime')
1901
+ Object.defineProperty(process, 'version', { value: 'v0.0.0' });
1902
+ else if (changed === 'producer') {
1903
+ assert.equal(existsSync(producerPath), false);
1904
+ writeFileSync(producerPath, '// changed synthetic runner identity\n');
1905
+ producerOwned = true;
1906
+ }
1907
+ else if (changed === 'bounds')
1908
+ input.resourceBounds.maxAggregateRssBytes -= 1;
1909
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] }), /Stage 'check' requires explicit source_only admission/u);
1910
+ for (const path of [counter, secondCounter, thirdCounter])
1911
+ assert.equal(readFileSync(path, 'utf8'), '1');
1912
+ }
1913
+ finally {
1914
+ if (previousPath === undefined)
1915
+ delete process.env.PATH;
1916
+ else
1917
+ process.env.PATH = previousPath;
1918
+ Object.defineProperty(process, 'version', versionDescriptor);
1919
+ if (producerOwned)
1920
+ rmSync(producerPath);
1921
+ rmSync(root, { recursive: true, force: true });
1922
+ }
1923
+ });
1924
+ test.each([
1925
+ 'stage',
1926
+ 'canonical',
1927
+ 'artifact',
1928
+ 'output',
1929
+ 'resource',
1930
+ 'resource-bounds',
1931
+ 'resource-samples',
1932
+ 'missing-resource',
1933
+ 'baseline',
1934
+ ])('serial admission rejects corrupt %s proof before considering execution', async (corrupt) => {
1935
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
1936
+ try {
1937
+ const row = await prepareIssueWorktree({
1938
+ baseRef: 'main',
1939
+ identity: 'synthetic-author',
1940
+ issueNumber: 17,
1941
+ repoRoot: root,
1942
+ });
1943
+ mkdirSync(join(row.path, '.issue-cli'), { recursive: true });
1944
+ const input = {
1945
+ issueNumber: 17,
1946
+ repoRoot: row.path,
1947
+ resourceBounds: {
1948
+ maxAggregateRssBytes: 1_000_000_000,
1949
+ minFreeDiskBytes: 1,
1950
+ maxNewOutputBytes: 1_000_000,
1951
+ outputRoots: ['.issue-cli'],
1952
+ },
1953
+ };
1954
+ const completed = await verifyIssue({
1955
+ ...input,
1956
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
1957
+ });
1958
+ const first = completed.stageReceipts[0];
1959
+ const common = join(root, '.git', 'ai-delivery');
1960
+ const id = first.input.inputId.slice(7);
1961
+ const stagePath = join(common, 'verification@1', 'stages', 'check', `${id}.json`);
1962
+ const resourcePath = join(common, 'resource-stages@1', `${id}.json`);
1963
+ if (corrupt === 'stage')
1964
+ writeFileSync(stagePath, 'corrupt stage proof\n');
1965
+ else if (corrupt === 'canonical')
1966
+ writeFileSync(stagePath, `${readFileSync(stagePath, 'utf8')}\n`);
1967
+ else if (corrupt === 'artifact') {
1968
+ const { receiptId: _receiptId, ...content } = first;
1969
+ const altered = { ...content, artifacts: [{ path: 'artifact.txt', digest: digestValue('corrupt artifact') }] };
1970
+ writeFileSync(stagePath, stableJson({ ...altered, receiptId: digestValue(altered) }));
1971
+ }
1972
+ else if (corrupt === 'output') {
1973
+ writeFileSync(join(common, 'verification@1', 'command-output', `${first.commands[0].outputDigest.slice(7)}.bin`), 'corrupt output');
1974
+ }
1975
+ else if (corrupt === 'resource')
1976
+ writeFileSync(resourcePath, 'corrupt resource proof\n');
1977
+ else if (corrupt === 'missing-resource')
1978
+ rmSync(resourcePath);
1979
+ else if (corrupt === 'baseline') {
1980
+ const directory = join(common, 'output-baselines@1');
1981
+ const file = readdirSync(directory).find((name) => name.endsWith('.json') && !name.endsWith('.state.json'));
1982
+ writeFileSync(join(directory, file), 'corrupt baseline proof\n');
1983
+ }
1984
+ else {
1985
+ const { checkpointId: _checkpointId, ...content } = JSON.parse(readFileSync(resourcePath, 'utf8'));
1986
+ if (corrupt === 'resource-bounds')
1987
+ content.resources.bounds.maxAggregateRssBytes += 1;
1988
+ else
1989
+ content.resources.sampleCount = 0;
1990
+ writeFileSync(resourcePath, JSON.stringify({ ...content, checkpointId: digestValue(content) }));
1991
+ }
1992
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] }), (error) => error instanceof Error && !error.message.includes('admission'));
1993
+ for (const path of [counter, secondCounter, thirdCounter])
1994
+ assert.equal(readFileSync(path, 'utf8'), '1');
1995
+ }
1996
+ finally {
1997
+ rmSync(root, { recursive: true, force: true });
1998
+ }
1999
+ });
2000
+ test('serial admission requires dependent execution after a real upstream rerun', async () => {
2001
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
2002
+ try {
2003
+ const row = await prepareIssueWorktree({
2004
+ baseRef: 'main',
2005
+ identity: 'synthetic-author',
2006
+ issueNumber: 17,
2007
+ repoRoot: root,
2008
+ });
2009
+ const input = {
2010
+ issueNumber: 17,
2011
+ repoRoot: row.path,
2012
+ resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
2013
+ };
2014
+ const completed = await verifyIssue({
2015
+ ...input,
2016
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
2017
+ });
2018
+ const id = completed.stageReceipts[0].input.inputId.slice(7);
2019
+ rmSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check', `${id}.json`));
2020
+ rmSync(join(root, '.git', 'ai-delivery', 'resource-stages@1', `${id}.json`));
2021
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['source_only'] }), /Stage 'second' requires explicit model admission/u);
2022
+ assert.equal(readFileSync(counter, 'utf8'), '2');
2023
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
2024
+ assert.equal(readFileSync(thirdCounter, 'utf8'), '1');
2025
+ }
2026
+ finally {
2027
+ rmSync(root, { recursive: true, force: true });
2028
+ }
2029
+ });
1806
2030
  test('bounded verification reruns unmeasured work and reuses only matching measured stages', async () => {
1807
2031
  const { root, counter, secondCounter, failSecond } = await fixture({ twoStages: true });
1808
2032
  try {