Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion python-scripts/ast_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ def function_to_dict(node: ast.FunctionDef | ast.AsyncFunctionDef) -> Dict[str,
"args": collect_args(node),
"returns": safe_unparse(node.returns),
"decorators": decorators,
"implementation_intent": extract_implementation_intent(node.decorator_list),
"docstring": ast.get_docstring(node),
"async": isinstance(node, ast.AsyncFunctionDef),
}


def class_to_dict(node: ast.ClassDef) -> Dict[str, Any]:
bases = [base for base in (safe_unparse(base) for base in node.bases) if base]
decorators = [
dec for dec in (safe_unparse(dec) for dec in node.decorator_list) if dec
]
methods = [
function_to_dict(stmt)
for stmt in node.body
Expand All @@ -125,6 +129,8 @@ def class_to_dict(node: ast.ClassDef) -> Dict[str, Any]:
"lineno": getattr(node, "lineno", 0),
"end_lineno": getattr(node, "end_lineno", getattr(node, "lineno", 0)),
"bases": bases,
"decorators": decorators,
"implementation_intent": extract_implementation_intent(node.decorator_list),
"methods": methods,
"docstring": ast.get_docstring(node),
}
Expand Down Expand Up @@ -159,6 +165,64 @@ def decorator_call(node: ast.AST) -> Optional[ast.Call]:
return node if isinstance(node, ast.Call) else None


def _decorator_name(node: ast.AST) -> Optional[str]:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
return node.attr
return None


def _string_arguments(call: ast.Call) -> List[str]:
values: List[str] = []

for arg in call.args:
value = literal_string(arg)
if value is not None:
values.append(value)
continue
values.extend(strings_from_iterable(arg))

for kw in call.keywords:
if kw.arg not in {"adr_id", "adr_ids", "adr", "adrs", "invariant", "invariants"}:
continue
value = literal_string(kw.value)
if value is not None:
values.append(value)
continue
values.extend(strings_from_iterable(kw.value))

return values


def extract_implementation_intent(decorators: Iterable[ast.AST]) -> Optional[Dict[str, Any]]:
attributed_adrs: List[str] = []
enforced_invariants: List[str] = []

for decorator in decorators:
call = decorator_call(decorator)
if not call:
continue
name = _decorator_name(call.func)
if name in {"implements_adr", "implements_adrs"}:
attributed_adrs.extend(_string_arguments(call))
elif name in {"enforces_invariant", "enforces_invariants"}:
enforced_invariants.extend(_string_arguments(call))

attributed_adrs = list(dict.fromkeys(attributed_adrs))
enforced_invariants = list(dict.fromkeys(enforced_invariants))

if not attributed_adrs and not enforced_invariants:
return None

return {
"implements_adrs": attributed_adrs,
"enforced_invariants": enforced_invariants,
"confidence": "declared",
"source": "decorator",
}


def extract_api_endpoints(
fn_nodes: Iterable[ast.FunctionDef | ast.AsyncFunctionDef],
) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -544,4 +608,3 @@ def main() -> None:

if __name__ == "__main__":
main()

113 changes: 113 additions & 0 deletions src/recon/implementation-intent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import fs from 'node:fs/promises';
import {
collectImplementationAttributionEvidence,
normalizeImplementationIntent,
writeImplementationAttributionEvidence,
} from './implementation-intent.js';
import type { NormalizedAssertion } from './phases/index.js';

vi.mock('node:fs/promises');

describe('implementation intent helpers', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
});

it('normalizes declared implementation intent objects', () => {
const normalized = normalizeImplementationIntent({
implements_adrs: ['ADR-L-0004'],
enforced_invariants: ['INV-0006'],
confidence: 'declared',
source: 'decorator',
});

expect(normalized).toEqual({
implements_adrs: ['ADR-L-0004'],
enforced_invariants: ['INV-0006'],
confidence: 'declared',
source: 'decorator',
});
});

it('collects evidence records from slices with implementation intent', () => {
const assertions: NormalizedAssertion[] = [
{
_slice: {
id: 'function:claims.py:process:12',
domain: 'graph',
type: 'function',
source_files: ['claims.py'],
},
element: {
id: 'function:claims.py:process:12',
name: 'process_claim',
implementation_intent: {
implements_adrs: ['ADR-L-0004', 'ADR-PC-0006'],
enforced_invariants: ['INV-0006'],
confidence: 'declared',
source: 'decorator',
},
},
provenance: {
extracted_at: '2026-03-14T00:00:00Z',
extractor: 'recon-python-extractor-v1',
file: 'claims.py',
line: 12,
language: 'python',
},
},
];

const evidence = collectImplementationAttributionEvidence(assertions);

expect(evidence.schema_version).toBe('1.0');
expect(evidence.type).toBe('implementation_attribution_evidence');
expect(evidence.records).toHaveLength(1);
expect(evidence.records[0].implementation_entity_type).toBe('function');
expect(evidence.records[0].attributed_adrs).toEqual(['ADR-L-0004', 'ADR-PC-0006']);
expect(evidence.records[0].enforced_invariants).toEqual(['INV-0006']);
expect(evidence.records[0].provenance.source_file).toBe('claims.py');
});

it('writes implementation attribution evidence to state', async () => {
const assertions: NormalizedAssertion[] = [
{
_slice: {
id: 'cfn_template:stack.yaml:stack',
domain: 'infrastructure',
type: 'template',
source_files: ['stack.yaml'],
},
element: {
id: 'cfn_template:stack.yaml:stack',
name: 'stack',
implementation_intent: {
implements_adrs: ['ADR-L-0004'],
enforced_invariants: [],
confidence: 'declared',
source: 'metadata',
},
},
provenance: {
extracted_at: '2026-03-14T00:00:00Z',
extractor: 'recon-cloudformation-extractor-v1',
file: 'stack.yaml',
line: 1,
language: 'cloudformation',
},
},
];

await writeImplementationAttributionEvidence('/tmp/.ste/state', assertions);

expect(fs.mkdir).toHaveBeenCalled();
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('implementation-attribution-evidence.yaml'),
expect.stringContaining('implementation_attribution_evidence'),
'utf-8',
);
});
});
172 changes: 172 additions & 0 deletions src/recon/implementation-intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import yaml from 'js-yaml';
import type { NormalizedAssertion } from './phases/index.js';

export interface ImplementationIntent {
implements_adrs: string[];
enforced_invariants: string[];
confidence: 'declared';
source: 'decorator' | 'metadata';
}

interface ImplementationAttributionRecord {
implementation_entity_id: string;
implementation_entity_type:
| 'function'
| 'class'
| 'module'
| 'service'
| 'workflow'
| 'infrastructure_template'
| 'configuration_file'
| 'schema_definition'
| 'pipeline'
| 'script'
| 'data_model';
attributed_adrs: string[];
enforced_invariants: string[];
provenance: {
source_file: string;
extractor: string;
commit: null;
};
metadata: Record<string, unknown>;
}

interface ImplementationAttributionEvidence {
schema_version: '1.0';
type: 'implementation_attribution_evidence';
records: ImplementationAttributionRecord[];
}

function normalizeStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}

return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
}

export function normalizeImplementationIntent(
value: unknown,
): ImplementationIntent | undefined {
if (!value || typeof value !== 'object') {
return undefined;
}

const record = value as Record<string, unknown>;
const implementsAdrs = normalizeStringList(record.implements_adrs);
const enforcedInvariants = normalizeStringList(record.enforced_invariants);
const confidence = record.confidence === 'declared' ? 'declared' : undefined;
const source =
record.source === 'decorator' || record.source === 'metadata'
? record.source
: undefined;

if (!confidence || !source) {
return undefined;
}

if (implementsAdrs.length === 0 && enforcedInvariants.length === 0) {
return undefined;
}

return {
implements_adrs: implementsAdrs,
enforced_invariants: enforcedInvariants,
confidence,
source,
};
}

function mapSliceTypeToEntityType(
assertion: NormalizedAssertion,
): ImplementationAttributionRecord['implementation_entity_type'] | undefined {
if (assertion._slice.domain === 'graph' && assertion._slice.type === 'function') {
return 'function';
}

if (assertion._slice.domain === 'graph' && assertion._slice.type === 'class') {
return 'class';
}

if (assertion._slice.domain === 'graph' && assertion._slice.type === 'module') {
return 'module';
}

if (assertion._slice.domain === 'data' && assertion._slice.type === 'entity') {
return 'data_model';
}

if (assertion._slice.domain === 'infrastructure' && assertion._slice.type === 'template') {
return 'infrastructure_template';
}

return undefined;
}

export function collectImplementationAttributionEvidence(
assertions: NormalizedAssertion[],
): ImplementationAttributionEvidence {
const records: ImplementationAttributionRecord[] = [];

for (const assertion of assertions) {
const intent = normalizeImplementationIntent(assertion.element.implementation_intent);
if (!intent) {
continue;
}

const implementationEntityType = mapSliceTypeToEntityType(assertion);
if (!implementationEntityType) {
continue;
}

records.push({
implementation_entity_id: String(assertion.element.id ?? assertion._slice.id),
implementation_entity_type: implementationEntityType,
attributed_adrs: intent.implements_adrs,
enforced_invariants: intent.enforced_invariants,
provenance: {
source_file: assertion.provenance.file,
extractor: assertion.provenance.extractor,
commit: null,
},
metadata: {
source: intent.source,
confidence: intent.confidence,
slice_id: assertion._slice.id,
},
});
}

records.sort((left, right) =>
left.implementation_entity_id.localeCompare(right.implementation_entity_id),
);

return {
schema_version: '1.0',
type: 'implementation_attribution_evidence',
records,
};
}

export async function writeImplementationAttributionEvidence(
stateDir: string,
assertions: NormalizedAssertion[],
): Promise<void> {
const evidence = collectImplementationAttributionEvidence(assertions);
const attributionDir = path.join(stateDir, 'attribution');
const targetPath = path.join(attributionDir, 'implementation-attribution-evidence.yaml');

await fs.mkdir(attributionDir, { recursive: true });
await fs.writeFile(
targetPath,
yaml.dump(evidence, {
noRefs: true,
lineWidth: -1,
sortKeys: false,
}),
'utf-8',
);
}
Loading
Loading