@maverickcer/env-cap 0.2.0 → 0.3.0

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/dist/build.cjs CHANGED
@@ -19,7 +19,7 @@ ${"-".repeat(Math.max(issue.variable.length,3))}
19
19
  Declared in:
20
20
  ${issue.files.join("\n")}`);return [header,...blocks].join("\n\n")}function countedHeader(prefix,singularNoun,issues){const noun=issues.length===1?singularNoun:`${singularNoun}s`;return `${prefix}
21
21
 
22
- ${issues.length} ${noun} found:`}var EnvManifestGenerationError=class _EnvManifestGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Environment manifest generation failed.","incompatible declaration",issues),issues));this.code="ENV_MANIFEST_GENERATION_FAILED";this.name="EnvManifestGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvManifestGenerationError);}};var EnvDocumentationGenerationError=class _EnvDocumentationGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Environment documentation generation failed.","documentation issue",issues),issues));this.code="ENV_DOCUMENTATION_GENERATION_FAILED";this.name="EnvDocumentationGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvDocumentationGenerationError);}};var EnvUsageAnalysisError=class _EnvUsageAnalysisError extends Error{constructor(issues){super(formatIssues(countedHeader("Dependency ownership report generation failed.","ownership issue",issues),issues));this.code="ENV_USAGE_ANALYSIS_FAILED";this.name="EnvUsageAnalysisError";this.issues=issues;Error.captureStackTrace?.(this,_EnvUsageAnalysisError);}};var EnvProjectGenerationError=class _EnvProjectGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Project generation failed.","blocking issue",issues),issues));this.code="ENV_PROJECT_GENERATION_FAILED";this.name="EnvProjectGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvProjectGenerationError);}};function detectExclusiveGroupIssues(contracts){const byGroup=new Map;for(const contract of contracts){if(!contract.active||!contract.exclusiveGroup)continue;const members=byGroup.get(contract.exclusiveGroup)??[];members.push(contract);byGroup.set(contract.exclusiveGroup,members);}const issues=[];for(const[group,members]of [...byGroup.entries()].sort((a,b)=>a[0].localeCompare(b[0]))){for(let i=0;i<members.length;i++){for(let j=i+1;j<members.length;j++){const a=members[i];const b=members[j];issues.push({severity:"error",variable:`Exclusive group "${group}"`,files:[a.file,b.file],reason:`"${a.contractName}" and "${b.contractName}" are both active and both declare exclusiveGroup "${group}" -- only one active contract per exclusive group is allowed. Set active: false on whichever one isn't in use.`});}}}return issues}function discoverValidationContexts(contracts){const contexts=new Set;for(const contract of contracts){if(!contract.active)continue;for(const variable of contract.variables){if(variable.context!==void 0)contexts.add(variable.context);}}return [...contexts].sort()}function renderManifest(contracts,outputFile){const sorted=[...contracts].sort((a,b)=>a.file.localeCompare(b.file));const outputDir=path6__default.default.dirname(outputFile);const usedNames=new Set;const importLines=[];const referenceNames=[];for(const contract of sorted){let localName=contract.exportName;let suffix=1;while(usedNames.has(localName)){localName=`${contract.exportName}_${suffix}`;suffix+=1;}usedNames.add(localName);referenceNames.push(localName);let specifier;if(contract.packageOrigin){specifier=contract.packageOrigin.packageName;}else {const withoutExt=contract.file.replace(/\.tsx?$/,"");specifier=path6__default.default.relative(outputDir,withoutExt).split(path6__default.default.sep).join("/");if(!specifier.startsWith("."))specifier=`./${specifier}`;}const binding=localName===contract.exportName?contract.exportName:`${contract.exportName} as ${localName}`;importLines.push(`import { ${binding} } from "${specifier}";`);}const activeContexts=discoverValidationContexts(contracts);const activeContextsLines=activeContexts.length>0?["// all currently active validation contexts",`export const activeContexts = [${activeContexts.map(c=>JSON.stringify(c)).join(", ")}];`,""]:[];const lines=[generatedBanner("ts"),"",...importLines,"",...activeContextsLines,"export const manifest = [",...referenceNames.map(name=>` ${name},`),"];",""];return lines.join("\n")}function defaultInclude(){return ["**/env.schema.ts"]}function defaultExclude(){return ["**/node_modules/**","**/dist/**","**/.git/**"]}function computeManifest(root,linkResult,onIncompatibility){const{contracts}=linkResult;const activeContracts=contracts.filter(contract=>contract.active);const compatibilityIssues=[...detectCompatibilityIssues(activeContracts),...detectExclusiveGroupIssues(activeContracts),...detectDuplicateVariableShapes(activeContracts)];const compatibilityErrors=compatibilityIssues.filter(issue=>issue.severity==="error");const blocking=onIncompatibility==="throw"?compatibilityIssues.filter(issue=>issue.severity!=="info"):compatibilityErrors;return {activeContracts,contractSummaries:contracts.map(contract=>summarizeContract(contract,root)),warnings:compatibilityIssues.filter(issue=>issue.severity==="warning"),blocking}}async function writeManifest(outputPath,activeContracts,fs){const manifestSource=renderManifest(activeContracts,outputPath);await fs.mkdir(path6__default.default.dirname(outputPath),{recursive:true});await fs.writeFile(outputPath,manifestSource,"utf8");}async function generateEnvManifest(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const onIncompatibility=options.onIncompatibility??"warn";const locationResult=resolveWithinRoot(root,options.location,"location","generateEnvManifest");if(!locationResult.ok)throw new EnvManifestGenerationError([locationResult.issue]);const outputPath=locationResult.resolved;const{linkResult,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const computed=computeManifest(root,linkResult,onIncompatibility);if(computed.blocking.length>0)throw new EnvManifestGenerationError(computed.blocking);await writeManifest(outputPath,computed.activeContracts,options.fs);return {outputPath,contracts:computed.contractSummaries,warnings:computed.warnings,parseWarnings:[...packageWarnings,...tsconfigWarnings,...linkResult.warnings]}}function sortedByContractName(entries){return [...entries].sort((a,b)=>a.contractName.localeCompare(b.contractName))}function renderDependencyOwnershipTable(entries){if(entries.length===0)return [];const lines=["## Dependency ownership","","Who owns each contract, who depends on it, and the blast radius if it changes.","","| Contract | Owner | Variables | Consumers | Blast radius |","|---|---|---|---|---|"];for(const entry of sortedByContractName(entries)){const consumers=entry.consumers.length>0?entry.consumers.join(", "):"--";lines.push(`| ${entry.contractName} (\`${entry.file}\`) | ${entry.owner??"--"} | ${entry.variableCount} | ${consumers} | ${entry.consumers.length} |`);}lines.push("");return lines}function renderAbandoned(findings){if(findings.length===0)return [];const lines=["## Abandoned ownership","","Contracts never imported anywhere in the scanned repository -- a feature's schema outliving the feature.","","| Contract | Owner | File |","|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| ${f.contractName} | ${f.owner??"--"} | \`${f.file}\` |`);lines.push("");return lines}function renderUnresolvedConsumers(findings){if(findings.length===0)return [];const lines=["## Unresolved consumers (barrel re-exports)","","Contracts reachable only through an unresolved `export * from` barrel re-export -- cannot be proven abandoned or consumed. Advisory only; never fails a build.",""];for(const f of sortedByContractName(findings))lines.push(`- **${f.contractName}** (\`${f.file}\`): ${f.reason}`);lines.push("");return lines}function formatStaleOrMissingCitations(citations){if(citations.length===0)return "--";return citations.map(c=>`${c.position.file}:${c.position.line}:${c.position.column} (${c.acknowledgment})`).join(", ")}function renderUnconsumedOwned(findings,scannedSurfaces){if(findings.length===0)return [];const surfaces=scannedSurfaces.map(s=>s.label).join(", ");const lines=["## Unconsumed owned dependencies","","Variables no consumer reads within the scanned surfaces below. Not proof of dead code. Common reasons a real consumer wouldn't show up here: it's read by a separate, out-of-repo service or webhook handler; it's consumed by non-TypeScript code (a shell script, a Dockerfile, a Terraform/Kubernetes manifest); or it's read from an allow-listed package whose directory wasn't included in this project's own `packages` configuration (ADR 0014).","",`Searched: ${surfaces}.`,"",'A blank Stale/missing citations cell is the strongest "looks genuinely unused" signal -- no developer has ever claimed otherwise. A non-blank cell means someone specifically claimed dynamic access here via `dynamicAccess`, and that claim can no longer be verified (ADR 0037) -- check with them before deleting.',"","| Variable | Contract | Owner | Stale/missing citations |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.owner??"--"} | ${formatStaleOrMissingCitations(f.staleOrMissingCitations)} |`);lines.push("");return lines}function renderIndeterminate(findings){if(findings.length===0)return [];const lines=["## Indeterminate (dynamic access)","","Dynamic (computed) property access was observed -- usage cannot be determined statically. Never guessed at.","","| Variable | Contract | Reason | Stale/missing citations |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.reason} | ${formatStaleOrMissingCitations(f.staleOrMissingCitations)} |`);lines.push("");return lines}function formatAssertions(assertions){const sites=assertions.map(a=>`${a.file}:${a.line}:${a.column}`).join(", ");return `Per developers, this data point is dynamically accessed at ${sites}.`}function renderAsserted(findings){if(findings.length===0)return [];const lines=["## Asserted (developer-acknowledged dynamic access)","","Variables env-cap's own scan would otherwise flag as unconsumed or indeterminate, but a developer has cited exactly where the dynamic access happens via `dynamicAccess`. The raw static status is shown alongside the citation, never hidden behind it -- a citation is a re-acknowledgment, not proof.","","| Variable | Contract | Static status | Developer assertion |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.wouldBeStatus} | ${formatAssertions(f.dynamicAccessAssertions)} |`);lines.push("");return lines}function renderParseWarnings(warnings){if(warnings.length===0)return [];const lines=["## Parse warnings","","Includes any `packages` (ADR 0014) resolution failures, alongside ordinary schema-discovery warnings.",""];for(const w of warnings)lines.push(`- **${w.file}**: ${w.message}`);lines.push("");return lines}function renderUsageReport(computed){const lines=[generatedBanner("markdown"),"",`> ${evidenceDisclaimer()}`,"",`> ${evidenceProjectionNote()}`,"","# Dependency & Ownership Report","","_Produced by `env-cap --ownership`._","","Which feature owns each variable, which features consume that contract, and what the blast radius is if it changes.","",...renderDependencyOwnershipTable(computed.dependencyOwnership),...renderAbandoned(computed.abandonedContracts),...renderUnresolvedConsumers(computed.unresolvedConsumers),...renderUnconsumedOwned(computed.unconsumedOwnedVariables,computed.scannedSurfaces),...renderIndeterminate(computed.indeterminate),...renderAsserted(computed.asserted),...renderParseWarnings(computed.parseWarnings)];return lines.join("\n").replace(/\n{3,}/g,"\n\n")}async function computeScanSurface(root,exclude,origins,fs){const localScanFiles=await discoverSchemaFiles({fs,root,include:["**/*.ts","**/*.tsx"],exclude});const scannedSurfaces=[{label:"application",root:"."}];const scanFiles=[...localScanFiles];const seenPackageDirs=new Set;for(const origin of origins.values()){if(seenPackageDirs.has(origin.packageDir))continue;seenPackageDirs.add(origin.packageDir);const packageScanFiles=await discoverSchemaFiles({fs,root:origin.packageDir,include:["**/*.ts","**/*.tsx"],exclude:defaultExclude()});scanFiles.push(...packageScanFiles);scannedSurfaces.push({label:`package:${origin.packageName}`,root:displayPath(root,origin.packageDir)});}return {scanFiles,scannedSurfaces}}async function computeUsage(root,contracts,scanFiles,readFile,context,parseWarnings,scannedSurfaces,dynamicAccessAcknowledgments){const graph=await buildDependencyGraph(contracts,scanFiles,readFile,context,scannedSurfaces,dynamicAccessAcknowledgments);const findings=deriveOwnershipFindings(graph);const contractByIdentity=new Map;for(const contract of contracts)contractByIdentity.set(`${contract.file}#${contract.exportName}`,contract);const ownerFor=(file,exportName)=>contractByIdentity.get(`${file}#${exportName}`)?.owner;const effectiveOwnerFor=(file,exportName,key)=>{const contract=contractByIdentity.get(`${file}#${exportName}`);if(!contract)return void 0;const variable=contract.variables.find(v=>v.key===key);return variable?effectiveOwner(contract,variable):contract.owner};const dependencyOwnership=graph.contracts.map(c=>({contractName:c.contractName,file:path6__default.default.relative(root,c.file),owner:ownerFor(c.file,c.exportName),variableCount:c.variables.size,consumers:c.consumingFiles.map(f=>path6__default.default.relative(root,f))}));const abandonedContracts=findings.abandoned.map(f=>({contractName:f.contractName,file:path6__default.default.relative(root,f.file),owner:ownerFor(f.file,f.exportName)}));const unresolvedConsumers=findings.unresolvedConsumers.map(f=>({contractName:f.contractName,file:path6__default.default.relative(root,f.file),reason:f.reason}));const staleOrMissingCitationsFor=(contractName,file,exportName,key)=>{const relativeFile=displayPath(root,file);const identity=dynamicAccessVariableIdentity(relativeFile,exportName,key);const problems=[];for(const a of dynamicAccessAcknowledgments?.get(identity)??[]){if(a.acknowledgment==="fresh")continue;problems.push({contractName,file:relativeFile,exportName,key,position:{file:a.file,line:a.line,column:a.column},acknowledgment:a.acknowledgment});}return problems};const unconsumedOwnedVariables=findings.unconsumedOwned.map(f=>({contractName:f.contractName,owner:effectiveOwnerFor(f.file,f.exportName,f.key),key:f.key,staleOrMissingCitations:staleOrMissingCitationsFor(f.contractName,f.file,f.exportName,f.key)}));const indeterminate=findings.indeterminate.map(f=>({contractName:f.contractName,key:f.key,reason:f.reason,dynamicAccessSites:f.dynamicAccessSites.map(p=>({...p,file:displayPath(root,p.file)})),staleOrMissingCitations:staleOrMissingCitationsFor(f.contractName,f.file,f.exportName,f.key)}));const asserted=findings.asserted.map(f=>({contractName:f.contractName,key:f.key,wouldBeStatus:f.wouldBeStatus,dynamicAccessAssertions:f.dynamicAccessAssertions}));return {graph,result:{dependencyOwnership,abandonedContracts,unresolvedConsumers,unconsumedOwnedVariables,indeterminate,asserted,parseWarnings,scannedSurfaces:graph.scannedSurfaces}}}async function writeUsageReport(reportPath,computed,fs){const source=renderUsageReport(computed);await fs.mkdir(path6__default.default.dirname(reportPath),{recursive:true});await fs.writeFile(reportPath,source,"utf8");}async function generateUsageReport(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];let reportPath;if(options.report){const result=resolveWithinRoot(root,options.report.location,"report.location","generateUsageReport");if(!result.ok)throw new EnvUsageAnalysisError([result.issue]);reportPath=result.resolved;}const{readFileCached,linkResult,context,origins,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const{scanFiles,scannedSurfaces}=await computeScanSurface(root,exclude,origins,options.fs);const computed=await computeUsage(root,linkResult.contracts,scanFiles,readFileCached,context,[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],scannedSurfaces);if(reportPath)await writeUsageReport(reportPath,computed.result,options.fs);return {reportPath,...computed.result}}var FINDING_MODEL_SCHEMA_VERSION=3;var NO_POSITION=void 0;function fromCompatibilityIssue(root,issue,code){return {severity:issue.severity,code,family:"compatibility",message:issue.reason,location:{model:"contract",file:displayPath(root,issue.files[0]),exportName:void 0,variable:issue.variable,position:NO_POSITION}}}function buildFindingModel(input){const{root}=input;const findings=[];for(const issue of input.compatibilityIssues??[]){findings.push(fromCompatibilityIssue(root,issue,issue.code??"DUPLICATE_VARIABLE_DOCUMENTATION"));}for(const issue of input.exclusiveGroupIssues??[]){findings.push(fromCompatibilityIssue(root,issue,"EXCLUSIVE_GROUP_VIOLATION"));}for(const check of input.artifactCheckFindings??[]){if(check.status==="ok")continue;findings.push({severity:"warning",code:check.status==="missing"?"ARTIFACT_MISSING":"ARTIFACT_STALE",family:"drift",message:check.detail??`${check.artifact} artifact is ${check.status}.`,location:{model:"change",path:check.path}});}const documentation=input.documentation;if(documentation){for(const c of documentation.undocumentedContracts){findings.push({severity:"warning",code:"UNDOCUMENTED_CONTRACT",family:"documentation",message:`"${c.exportName}" has no documentEnv() call linked to it.`,location:{model:"contract",file:displayPath(root,c.file),exportName:c.exportName,variable:void 0,position:NO_POSITION}});}for(const v of documentation.undocumentedVariables){findings.push({severity:"warning",code:"UNDOCUMENTED_VARIABLE",family:"documentation",message:`"${v.key}" (declared by "${v.exportName}") has no matching entry in a linked documentEnv()'s "variables".`,location:{model:"contract",file:displayPath(root,v.file),exportName:v.exportName,variable:v.key,position:NO_POSITION}});}for(const s of documentation.staleDocEntries){findings.push({severity:"warning",code:"STALE_DOC_ENTRY",family:"documentation",message:`"${s.key}" is documented under "${s.exportName}" but no longer exists in that contract's schema.`,location:{model:"contract",file:displayPath(root,s.file),exportName:s.exportName,variable:s.key,position:NO_POSITION}});}for(const e of documentation.expiringSoon){const expired=e.daysRemaining<0;findings.push({severity:"warning",code:expired?"EXPIRED":"EXPIRING_SOON",family:"documentation",message:expired?`"${e.key??e.exportName}" expired ${Math.abs(e.daysRemaining)} day(s) ago (expiresAt: ${e.expiresAt}).`:`"${e.key??e.exportName}" expires in ${e.daysRemaining} day(s) (expiresAt: ${e.expiresAt}).`,location:{model:"contract",file:displayPath(root,e.file),exportName:e.exportName,variable:e.key,position:NO_POSITION}});}for(const n of documentation.nonstandardSensitivityLevels){findings.push({severity:"info",code:"NONSTANDARD_SENSITIVITY_LEVEL",family:"documentation",message:`"${n.key??n.exportName}" declares sensitivity "${n.sensitivity}", which isn't one of the standard levels (secret/credential/pii/config) -- still honored verbatim, just flagged for vocabulary drift.`,location:{model:"contract",file:displayPath(root,n.file),exportName:n.exportName,variable:n.key,position:NO_POSITION}});}for(const u of documentation.unresolvedLinks){findings.push({severity:"warning",code:"UNRESOLVED_DOCUMENTENV_LINK",family:"documentation",message:u.reason,location:{model:"contract",file:displayPath(root,u.file),exportName:void 0,variable:void 0,position:NO_POSITION}});}}for(const a of input.abandonedContracts??[]){findings.push({severity:"warning",code:"ABANDONED_CONTRACT",family:"ownership",message:`"${a.contractName}" is never imported anywhere in the scanned repository.`,location:{model:"ownership",contractName:a.contractName,file:a.file,variable:void 0,position:NO_POSITION}});}for(const u of input.unresolvedConsumers??[]){findings.push({severity:"warning",code:"UNRESOLVED_CONSUMER",family:"ownership",message:u.reason,location:{model:"ownership",contractName:u.contractName,file:u.file,variable:void 0,position:NO_POSITION}});}for(const u of input.unconsumedOwnedVariables??[]){findings.push({severity:"warning",code:"UNCONSUMED_OWNED_VARIABLE",family:"ownership",message:`"${u.key}" (declared by "${u.contractName}") has no consumer found in the scanned repository.`,location:{model:"ownership",contractName:u.contractName,file:void 0,variable:u.key,position:NO_POSITION}});}for(const i of input.indeterminateOwnership??[]){findings.push({severity:"warning",code:"INDETERMINATE_OWNERSHIP",family:"ownership",message:i.reason,location:{model:"ownership",contractName:i.contractName,file:void 0,variable:i.key,position:NO_POSITION}});}for(const p of input.dynamicAccessCitationProblems??[]){const missing=p.acknowledgment==="missing";findings.push({severity:"warning",code:missing?"MISSING_DYNAMIC_ACCESS_CITATION":"STALE_DYNAMIC_ACCESS_CITATION",family:"ownership",message:missing?`"${p.key}" (declared by "${p.contractName}") cites dynamic access at ${p.position.file}:${p.position.line}:${p.position.column}, but that file no longer exists. Re-run generate:env once the citation is corrected.`:`"${p.key}" (declared by "${p.contractName}") cites dynamic access at ${p.position.file}:${p.position.line}:${p.position.column}, but that file's content has changed since it was last acknowledged. Re-run generate:env once you've re-confirmed the citation still applies.`,location:{model:"ownership",contractName:p.contractName,file:p.file,variable:p.key,position:p.position}});}return {schemaVersion:FINDING_MODEL_SCHEMA_VERSION,findings}}function humanizeKey(key){const spaced=key.replace(/([a-z0-9])([A-Z])/g,"$1 $2");return spaced.charAt(0).toUpperCase()+spaced.slice(1)}function renderMetadataValue(value){return typeof value==="string"?value:JSON.stringify(value)}function defineEvidenceProjection(schema){function project(evidence){const value={};const sources={};for(const key of Object.keys(schema)){const projector=schema[key];const{proxy,readPaths}=createTrackingProxy(evidence);value[key]=projector(proxy);sources[key]=readPaths();}return {value,sources}}function invoke(evidence){return project(evidence).value}return Object.assign(invoke,{project})}function readOnlyMembraneError(){return new TypeError("EvidenceModel is read-only inside a projector -- a projection must be a pure function of its evidence argument. See ADR 0032.")}function createTrackingProxy(evidence){const clone=structuredClone(evidence);const paths=new Set;const wrapped=new WeakMap;function wrap(value,path18){if(value===null||typeof value!=="object")return value;const target=value;const cached=wrapped.get(target);if(cached)return cached;const proxy=new Proxy(target,{get(currentTarget,prop,receiver){const result=Reflect.get(currentTarget,prop,receiver);if(typeof prop!=="string")return result;if(Array.isArray(currentTarget)&&prop==="length")return result;const nextPath=[...path18,prop];paths.add(nextPath.join("."));return wrap(result,nextPath)},set(){throw readOnlyMembraneError()},deleteProperty(){throw readOnlyMembraneError()},defineProperty(){throw readOnlyMembraneError()},setPrototypeOf(){throw readOnlyMembraneError()}});wrapped.set(target,proxy);return proxy}return {proxy:wrap(clone,[]),readPaths:()=>[...paths].sort()}}function byContractThenKey(a,b){return a.file.localeCompare(b.file)||a.exportName.localeCompare(b.exportName)||a.key.localeCompare(b.key)}function contractIndex(contracts){return new Map(contracts.map(c=>[`${c.file}#${c.exportName}`,c]))}function groupVariablesByOwner(contracts,ownerOf){const byOwner=new Map;for(const contract of contracts){for(const variable of contract.variables){const owner=ownerOf(contract,variable);if(owner===void 0)continue;const list=byOwner.get(owner)??[];list.push({contract,variable});byOwner.set(owner,list);}}return byOwner}var configurationReference=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),entries:evidence=>{const entries=[];for(const contract of evidence.contract.contracts){for(const variable of contract.variables){entries.push({file:contract.file,exportName:contract.exportName,contractName:contract.contractName,key:variable.key,description:variable.description,owner:variable.owner??contract.owner,sensitivity:variable.sensitivity??contract.sensitivity,required:variable.required,hasDefault:variable.hasDefault,hasProcessor:variable.hasProcessor,hasValidator:variable.hasValidator,expiresAt:variable.expiresAt,active:contract.active});}}return entries.sort(byContractThenKey)}});var ownershipSummary=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),owners:evidence=>{const contracts=contractIndex(evidence.contract.contracts);const nameOf=(file,exportName)=>contracts.get(`${file}#${exportName}`)?.contractName??exportName;const variablesByOwner=groupVariablesByOwner(evidence.ownership.contracts,(_contract,variable)=>variable.owner);const contractsByOwner=new Map;for(const contract of evidence.ownership.contracts){if(contract.owner===void 0)continue;const list=contractsByOwner.get(contract.owner)??[];list.push(nameOf(contract.file,contract.exportName));contractsByOwner.set(contract.owner,list);}const owners=new Set([...variablesByOwner.keys(),...contractsByOwner.keys()]);return [...owners].sort().map(owner=>({owner,variables:(variablesByOwner.get(owner)??[]).map(({contract,variable})=>`${nameOf(contract.file,contract.exportName)}.${variable.key}`).sort(),contracts:(contractsByOwner.get(owner)??[]).sort()}))},unowned:evidence=>{const contracts=contractIndex(evidence.contract.contracts);return evidence.ownership.unownedVariables.map(ref=>{const contractName=contracts.get(`${ref.file}#${ref.exportName}`)?.contractName??ref.exportName;return `${contractName}.${ref.key}`}).sort()}});var expiringSoonReport=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),entries:evidence=>{const contracts=contractIndex(evidence.contract.contracts);const ownerByVariable=new Map;for(const contract of evidence.ownership.contracts)for(const variable of contract.variables)ownerByVariable.set(`${contract.file}#${contract.exportName}#${variable.key}`,variable.owner);return evidence.lifecycle.expiring.map(entry=>{const contract=contracts.get(`${entry.file}#${entry.exportName}`);const variable=entry.key===void 0?void 0:contract?.variables.find(v=>v.key===entry.key);return {file:entry.file,exportName:entry.exportName,contractName:contract?.contractName??entry.exportName,key:entry.key,expiresAt:entry.expiresAt,daysRemaining:entry.daysRemaining,expired:entry.daysRemaining<0,refreshInstructions:variable?.refreshInstructions,owner:entry.key===void 0?contract?.owner:ownerByVariable.get(`${entry.file}#${entry.exportName}#${entry.key}`)??contract?.owner}})},expiredCount:evidence=>evidence.lifecycle.expiring.filter(e=>e.daysRemaining<0).length});function parseIsoDate(value){const date=new Date(value);return Number.isNaN(date.getTime())?void 0:date}var MS_PER_DAY=864e5;function daysRemainingFrom(date,now){return Math.ceil((date.getTime()-now.getTime())/MS_PER_DAY)}function computeExpiringEntries(contracts,expiringWithinDays,now){const entries=[];for(const contract of contracts){if(contract.expiresAt){const date=parseIsoDate(contract.expiresAt);if(date){const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<=expiringWithinDays){entries.push({file:contract.file,exportName:contract.exportName,key:void 0,expiresAt:contract.expiresAt,daysRemaining});}}}for(const variable of contract.variables){if(!variable.expiresAt)continue;const date=parseIsoDate(variable.expiresAt);if(!date)continue;const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<=expiringWithinDays){entries.push({file:contract.file,exportName:contract.exportName,key:variable.key,expiresAt:variable.expiresAt,daysRemaining});}}}return entries.sort((a,b)=>a.daysRemaining-b.daysRemaining)}function buildCatalog(contracts){return sortedContracts(contracts).map(contract=>{const variables={};for(const variable of contract.variables){variables[variable.key]={description:variable.description,owner:effectiveOwner(contract,variable),sensitivity:effectiveSensitivity(contract,variable),expiresAt:variable.expiresAt,refreshInstructions:variable.refreshInstructions,setupInstructions:variable.setupInstructions,required:variable.required,hasDefault:variable.hasDefault,hasProcessor:variable.hasProcessor,processorReturnType:variable.processorReturnType,hasValidator:variable.hasValidator,documented:variable.documented,context:variable.context,purpose:effectivePurpose(contract,variable),legalBasis:effectiveLegalBasis(contract,variable),retention:effectiveRetention(contract,variable),dataResidency:effectiveDataResidency(contract,variable),auditRequired:effectiveAuditRequired(contract,variable),metadata:variable.metadata,evidence:variable.evidence};}return {file:contract.file,exportName:contract.exportName,contractName:contract.contractName,active:contract.active,documented:contract.documented,category:contract.category,exclusiveGroup:contract.exclusiveGroup,...governanceFieldsOf(contract),variables}})}function extractPreviouslyDocumentedKeys(previousContent){const keys=new Set;for(const match of previousContent.matchAll(/^### `([A-Za-z_][A-Za-z0-9_]*)`$/gm)){if(match[1])keys.add(match[1]);}return keys}function extractPreviouslyActiveKeys(previousContent){const keys=new Set;let currentContractActive=false;for(const line of previousContent.split("\n")){const activeMatch=/^- Active: (yes|no)$/.exec(line);if(activeMatch){currentContractActive=activeMatch[1]==="yes";continue}const headingMatch=/^### `([A-Za-z_][A-Za-z0-9_]*)`$/.exec(line);if(headingMatch&&currentContractActive){keys.add(headingMatch[1]);}}return keys}function computeChangeSummary(contracts,previousContent){if(previousContent===void 0)return {added:[],removed:[],commented:[]};const previousKeys=extractPreviouslyDocumentedKeys(previousContent);const previousActiveKeys=extractPreviouslyActiveKeys(previousContent);const currentKeys=new Set;const currentActiveKeys=new Set;for(const contract of contracts){for(const variable of contract.variables){currentKeys.add(variable.key);if(contract.active)currentActiveKeys.add(variable.key);}}return {added:[...currentKeys].filter(k=>!previousKeys.has(k)).sort(),removed:[...previousKeys].filter(k=>!currentKeys.has(k)).sort(),commented:[...previousActiveKeys].filter(k=>currentKeys.has(k)&&!currentActiveKeys.has(k)).sort()}}function slugify(text){return text.toLowerCase().replace(/[^a-z0-9_]+/g,"-").replace(/^-+|-+$/g,"")||"section"}var AnchorRegistry=class{constructor(){this.used=new Map;this.assigned=new Map;}anchorFor(target,baseText){const existing=this.assigned.get(target);if(existing)return existing;const base=slugify(baseText);const count=this.used.get(base)??0;this.used.set(base,count+1);const anchor=count===0?base:`${base}-${count}`;this.assigned.set(target,anchor);return anchor}};function mdLink(text,anchor){return `[${text}](#${anchor})`}function sortedContracts(contracts){return [...contracts].sort((a,b)=>a.contractName.localeCompare(b.contractName))}function renderHeader(contracts,options,anchors,hasOwnership,hasDuplicates,hasLifecycle){const lines=[generatedBanner("markdown"),"",`> ${evidenceDisclaimer()}`,"",`> ${evidenceProjectionNote()}`,"","# Environment Variables","","_Produced by `env-cap --docs`._","",`_Generated ${options.generatedAt.toISOString()}_`,""];const summary=computeChangeSummary(contracts,options.previousContent);lines.push("## Changes since last report","");if(summary.added.length===0&&summary.removed.length===0&&summary.commented.length===0){lines.push("No changes.","");}else {if(summary.added.length>0)lines.push(`- **Added:** ${summary.added.map(k=>`\`${k}\``).join(", ")}`);if(summary.removed.length>0)lines.push(`- **Removed:** ${summary.removed.map(k=>`\`${k}\``).join(", ")}`);if(summary.commented.length>0)lines.push(`- **No longer required (inactive):** ${summary.commented.map(k=>`\`${k}\``).join(", ")}`);lines.push("");}lines.push("## Table of contents","",`- ${mdLink("Catalog","catalog")}`);for(const contract of sortedContracts(contracts)){lines.push(` - ${mdLink(contract.contractName,anchors.anchorFor(contract,`contract-${contract.contractName}`))}`);}if(hasOwnership)lines.push(`- ${mdLink("Ownership matrix","ownership-matrix")}`);if(hasDuplicates)lines.push(`- ${mdLink("Dependency graph","dependency-graph")}`);if(hasLifecycle)lines.push(`- ${mdLink("Lifecycle report","lifecycle-report")}`);lines.push(`- ${mdLink("Security review","security-review")}`,"");return lines}function renderCatalog(contracts,anchors,undocumented){const lines=["## Catalog","",'<a id="catalog"></a>',""];const undocumentedByIdentity=new Set(undocumented.map(u=>`${u.file}#${u.exportName}`));const usesValidationContexts=contracts.some(c=>c.variables.some(v=>v.context));if(usesValidationContexts){lines.push("> Validation contexts describe when validation participates. They do not restrict access to values, and they do not remove a variable's schema (or its `default` value) from wherever this manifest is imported.","");}for(const contract of sortedContracts(contracts)){lines.push(`<a id="${anchors.anchorFor(contract,`contract-${contract.contractName}`)}"></a>`);lines.push(`## ${contract.contractName}`,"",`Source: \`${contract.file}\``);if(undocumentedByIdentity.has(`${contract.file}#${contract.exportName}`)){lines.push("","> \u26A0\uFE0F **Undocumented.** No `documentEnv()` call is linked to this contract.");}lines.push(`- Active: ${contract.active?"yes":"no"}`);if(contract.category!==void 0)lines.push(`- Category: ${contract.category}`);if(contract.exclusiveGroup!==void 0)lines.push(`- Exclusive group: ${contract.exclusiveGroup}`);if(contract.owner!==void 0)lines.push(`- Owner: ${contract.owner}`);if(contract.sensitivity!==void 0)lines.push(`- Sensitivity: ${contract.sensitivity}`);if(contract.expiresAt!==void 0)lines.push(`- Expires: ${contract.expiresAt}`);if(contract.purpose!==void 0)lines.push(`- Purpose: ${contract.purpose}`);if(contract.legalBasis!==void 0)lines.push(`- Legal basis: ${contract.legalBasis}`);if(contract.retention!==void 0)lines.push(`- Retention: ${contract.retention}`);if(contract.dataResidency!==void 0)lines.push(`- Data residency: ${renderMetadataValue(contract.dataResidency)}`);if(contract.auditRequired!==void 0)lines.push(`- Audit required: ${contract.auditRequired?"yes":"no"}`);if(contract.metadata){for(const[metaKey,metaValue]of Object.entries(contract.metadata)){lines.push(`- ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}}lines.push("");for(const variable of [...contract.variables].sort((a,b)=>a.key<b.key?-1:1)){lines.push(`<a id="${anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`)}"></a>`);lines.push(`### \`${variable.key}\``,"");if(variable.description)lines.push(variable.description,"");if(!variable.documented)lines.push("> \u26A0\uFE0F **Undocumented.**","");lines.push(`- Default: ${variable.hasDefault?"yes":"no"}`,`- Processor: ${variable.hasProcessor?"yes":"no"}`,`- Validator: ${variable.hasValidator?"yes":"no"}`);if(variable.context!==void 0)lines.push(`- Validation context: ${variable.context}`);const owner=effectiveOwner(contract,variable);if(owner!==void 0)lines.push(`- Owner: ${owner}`);const sensitivity=effectiveSensitivity(contract,variable);if(sensitivity!==void 0)lines.push(`- Sensitivity: ${sensitivity}`);if(variable.expiresAt!==void 0)lines.push(`- Expires: ${variable.expiresAt}`);if(variable.setupInstructions!==void 0)lines.push(`- Setup instructions: ${variable.setupInstructions}`);if(variable.refreshInstructions!==void 0)lines.push(`- Refresh instructions: ${variable.refreshInstructions}`);if(variable.required!==void 0)lines.push(`- Required: ${variable.required?"yes":"no"}`);const purpose=effectivePurpose(contract,variable);if(purpose!==void 0)lines.push(`- Purpose: ${purpose}`);const legalBasis=effectiveLegalBasis(contract,variable);if(legalBasis!==void 0)lines.push(`- Legal basis: ${legalBasis}`);const retention=effectiveRetention(contract,variable);if(retention!==void 0)lines.push(`- Retention: ${retention}`);const dataResidency=effectiveDataResidency(contract,variable);if(dataResidency!==void 0)lines.push(`- Data residency: ${renderMetadataValue(dataResidency)}`);const auditRequired=effectiveAuditRequired(contract,variable);if(auditRequired!==void 0)lines.push(`- Audit required: ${auditRequired?"yes":"no"}`);const dynamicAccess=variable.evidence?.dynamicAccess;if(dynamicAccess&&dynamicAccess.length>0)lines.push(`- Dynamic access: ${dynamicAccess.join(", ")}`);for(const[metaKey,metaValue]of Object.entries(variable.metadata??{})){lines.push(`- ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}lines.push("");}}return lines}function renderOwnershipMatrix(contracts,anchors){const grouped=groupVariablesByOwner(sortedContracts(contracts),effectiveOwner);const byOwner=new Map;for(const[owner,entries]of grouped){byOwner.set(owner,entries.map(({contract,variable})=>mdLink(`\`${variable.key}\` (${contract.contractName})`,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))));}if(byOwner.size===0)return [];const lines=["## Ownership matrix","",'<a id="ownership-matrix"></a>',"","| Owner | Variables |","|---|---|"];for(const owner of [...byOwner.keys()].sort()){lines.push(`| ${owner} | ${mustGet(byOwner,owner).join(", ")} |`);}lines.push("");return lines}function renderDependencyGraph(contracts,anchors){const byKey=new Map;for(const contract of contracts){for(const variable of contract.variables){const list=byKey.get(variable.key)??[];list.push({contract,variable});byKey.set(variable.key,list);}}const hasDuplicates=[...byKey.values()].some(declarations=>declarations.length>1);const lines=["## Dependency graph","",'<a id="dependency-graph"></a>',"",`One row per unique variable name; more than one location means more than one feature declares it (see the package README's "Duplicate variables" section for how that's handled at runtime).`,"","| Variable | Declared in |","|---|---|"];for(const key of [...byKey.keys()].sort()){const declarations=mustGet(byKey,key);const locations=declarations.map(({contract,variable})=>mdLink(`${contract.contractName} (\`${contract.file}\`)`,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))).join(", ");lines.push(`| \`${key}\` | ${locations} |`);}lines.push("");return {lines,hasDuplicates}}function renderLifecycleReport(contracts,anchors,expiringWithinDays,now){const rows=[];for(const contract of sortedContracts(contracts)){for(const variable of contract.variables){const owner=effectiveOwner(contract,variable);if(variable.expiresAt===void 0&&owner===void 0&&variable.refreshInstructions===void 0)continue;const expiresAt=variable.expiresAt;const expiry=expiresAt?parseIsoDate(expiresAt):void 0;let expiresCell=expiresAt??"--";if(expiry&&expiresAt){const daysRemaining=daysRemainingFrom(expiry,now);if(daysRemaining<0)expiresCell=`${expiresAt} (**expired ${Math.abs(daysRemaining)}d ago**)`;else if(daysRemaining<=expiringWithinDays)expiresCell=`${expiresAt} (**${daysRemaining}d remaining**)`;}rows.push(`| ${mdLink(`\`${variable.key}\``,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))} | ${owner??"--"} | ${expiresCell} | ${variable.refreshInstructions??"--"} |`);}}if(rows.length===0)return {lines:[],hasLifecycle:false};const lines=["## Lifecycle report","",'<a id="lifecycle-report"></a>',"","| Variable | Owner | Expires | Refresh instructions |","|---|---|---|---|",...rows,""];return {lines,hasLifecycle:true}}function computeSecurityReviewCounters(contracts,expiringWithinDays,now,undocumentedContractCount,undocumentedVariableCount){let totalVariableDeclarations=0;let activeVariableDeclarations=0;const uniqueKeys=new Set;const keyContractCount=new Map;let expiresAtSetCount=0;let expiredCount=0;let expiringSoonCount=0;let requiredCount=0;let refreshInstructionsCount=0;let noOwnerCount=0;for(const contract of contracts){for(const variable of contract.variables){totalVariableDeclarations+=1;if(contract.active)activeVariableDeclarations+=1;uniqueKeys.add(variable.key);keyContractCount.set(variable.key,(keyContractCount.get(variable.key)??0)+1);if(variable.required)requiredCount+=1;if(variable.refreshInstructions)refreshInstructionsCount+=1;if(effectiveOwner(contract,variable)===void 0)noOwnerCount+=1;if(variable.expiresAt){expiresAtSetCount+=1;const date=parseIsoDate(variable.expiresAt);if(date){const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<0)expiredCount+=1;else if(daysRemaining<=expiringWithinDays)expiringSoonCount+=1;}}}}const duplicateVariableNameCount=[...keyContractCount.values()].filter(count=>count>1).length;return {totalContracts:contracts.length,totalVariableDeclarations,activeVariableDeclarations,uniqueVariableNames:uniqueKeys.size,expiresAtSetCount,expiredCount,expiringSoonCount,requiredCount,refreshInstructionsCount,noOwnerCount,duplicateVariableNameCount,undocumentedContractCount,undocumentedVariableCount}}function renderSecurityReview(contracts,options,now){const counters=computeSecurityReviewCounters(contracts,options.expiringWithinDays,now,options.undocumentedContracts.length,options.undocumentedVariables.length);return ["## Security review","",'<a id="security-review"></a>',"",`- Total contracts: ${counters.totalContracts}`,`- Total variable declarations: ${counters.totalVariableDeclarations} (${counters.activeVariableDeclarations} from active contracts)`,`- Unique variable names: ${counters.uniqueVariableNames}`,`- Variables with \`expiresAt\` set: ${counters.expiresAtSetCount}`,` - Already expired: ${counters.expiredCount}`,` - Expiring within ${options.expiringWithinDays} days: ${counters.expiringSoonCount}`,`- Variables marked \`required: true\`: ${counters.requiredCount}`,`- Variables with refresh instructions: ${counters.refreshInstructionsCount}`,`- Variables with no assigned owner: ${counters.noOwnerCount}`,`- Variable names declared by more than one contract: ${counters.duplicateVariableNameCount}`,`- Undocumented contracts: ${counters.undocumentedContractCount}`,`- Undocumented variables: ${counters.undocumentedVariableCount}`,""]}function renderDocs(contracts,options){const anchors=new AnchorRegistry;const sorted=sortedContracts(contracts);const ownership=renderOwnershipMatrix(sorted,anchors);const{lines:dependencyLines,hasDuplicates}=renderDependencyGraph(sorted,anchors);const{lines:lifecycleLines,hasLifecycle}=renderLifecycleReport(sorted,anchors,options.expiringWithinDays,options.generatedAt);const header=renderHeader(sorted,options,anchors,ownership.length>0,hasDuplicates,hasLifecycle);const catalog=renderCatalog(sorted,anchors,options.undocumentedContracts);const security=renderSecurityReview(sorted,options,options.generatedAt);return [...header,...catalog,...ownership,...dependencyLines,...lifecycleLines,...security].join("\n").replace(/\n{3,}/g,"\n\n")}function normalizeDocsForComparison(content){return content.replace(/^_Generated .+_$/m,"_Generated <normalized-for-comparison>_").replace(/\(\*\*\d+d remaining\*\*\)/g,"(**Nd remaining**)").replace(/\(\*\*expired \d+d ago\*\*\)/g,"(**expired Nd ago**)").replace(/^(- Already expired:) \d+$/gm,"$1 N").replace(/^(- Expiring within \d+ days:) \d+$/gm,"$1 N")}function isPrimitiveLiteral(value){return typeof value==="string"||typeof value==="number"||typeof value==="boolean"}function defaultLiteralFor(variable){return variable.defaultValue?.ok===true&&isPrimitiveLiteral(variable.defaultValue.value)?String(variable.defaultValue.value):void 0}function renderVariableLines(variable,options={}){const lines=[];if(options.note)lines.push(`# ${options.note}`);if(variable.description)lines.push(`# ${variable.description}`);if(variable.context)lines.push(`# Validation context: ${variable.context}`);if(variable.owner)lines.push(`# Owner: ${variable.owner}`);if(variable.setupInstructions)lines.push(`# Setup: ${variable.setupInstructions}`);if(variable.expiresAt)lines.push(`# Expires At: ${variable.expiresAt}`);if(variable.refreshInstructions)lines.push(`# Refresh Instructions: ${variable.refreshInstructions}`);if(variable.required)lines.push(`# Required: yes`);if(variable.purpose)lines.push(`# Purpose: ${variable.purpose}`);if(variable.legalBasis)lines.push(`# Legal Basis: ${variable.legalBasis}`);if(variable.retention)lines.push(`# Retention Policy: ${variable.retention}`);if(variable.dataResidency)lines.push(`# Data Residency: ${renderMetadataValue(variable.dataResidency)}`);if(variable.auditRequired)lines.push(`# Audit Required: yes`);for(const[metaKey,metaValue]of Object.entries(variable.metadata??{})){lines.push(`# ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}const prefix=options.commented?"# ":"";lines.push(`${prefix}${variable.key}=${defaultLiteralFor(variable)??""}`,"");return lines}function groupByKey(contracts){const byKey=new Map;for(const contract of contracts){for(const variable of contract.variables){const list=byKey.get(variable.key)??[];list.push({contract,variable});byKey.set(variable.key,list);}}return byKey}function renderEnvExample(contracts,reconciliationHeader=[]){const sorted=[...contracts].sort((a,b)=>a.file.localeCompare(b.file));const active=sorted.filter(c=>c.active);const inactive=sorted.filter(c=>!c.active);const lines=["# AUTO-GENERATED EXAMPLE FILE.","# Copy to .env and fill in real values. Do not commit .env."];if(sorted.some(c=>c.variables.some(v=>v.context))){lines.push("# Validation context annotations describe when validation participates.","# They do not restrict access to values, and every variable below is","# still written to this file regardless of its context.");}if(reconciliationHeader.length>0)lines.push(...reconciliationHeader);lines.push("");const activeByKey=groupByKey(active);for(const key of [...activeByKey.keys()].sort()){const[first,...rest]=mustGet(activeByKey,key);lines.push(...renderVariableLines(first.variable));for(const dup of rest){lines.push(...renderVariableLines(dup.variable,{commented:true,note:`Also declared by "${dup.contract.contractName}" -- see "${first.contract.contractName}" above.`}));}}const inactiveByKey=groupByKey(inactive);for(const key of [...inactiveByKey.keys()].sort()){if(activeByKey.has(key))continue;const[first]=mustGet(inactiveByKey,key);lines.push(...renderVariableLines(first.variable,{commented:true,note:`Disabled -- feature "${first.contract.contractName}" is not active.`}));}return lines.join("\n").replace(/\n+$/,"\n")}function extractDeclaredVariables(source){const names=[];for(const line of source.split("\n")){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;const match=/^([A-Za-z_][A-Za-z0-9_]*)=/.exec(trimmed);if(match?.[1])names.push(match[1]);}return names}function extractCommentedVariables(source){const names=[];for(const line of source.split("\n")){const trimmed=line.trim();if(!trimmed.startsWith("#"))continue;const withoutHash=trimmed.replace(/^#+\s*/,"");const match=/^([A-Za-z_][A-Za-z0-9_]*)=/.exec(withoutHash);if(match?.[1])names.push(match[1]);}return names}function computeReconciliation(contracts,existingContent){const activeKeys=new Set(contracts.filter(c=>c.active).flatMap(c=>c.variables.map(v=>v.key)));const knownKeys=new Set(contracts.flatMap(c=>c.variables.map(v=>v.key)));const oldLive=new Set(extractDeclaredVariables(existingContent));const oldKnown=new Set([...oldLive,...extractCommentedVariables(existingContent)]);const staleVariables=[...oldKnown].filter(key=>!knownKeys.has(key)).sort();const variablesToComment=[...oldLive].filter(key=>!activeKeys.has(key)&&knownKeys.has(key)).sort();const variablesToAdd=[...activeKeys].filter(key=>!oldLive.has(key)).sort();return {staleVariables,variablesToComment,variablesToAdd}}function renderReconciliationHeader(reconciliation){const lines=[];if(reconciliation.staleVariables.length>0){lines.push("#","# Remove the following variables (no longer declared by any feature):");for(const key of reconciliation.staleVariables)lines.push(`# - ${key}`);}if(reconciliation.variablesToComment.length>0){lines.push("#","# Comment the following variables (their feature is no longer active):");for(const key of reconciliation.variablesToComment)lines.push(`# - ${key}`);}if(reconciliation.variablesToAdd.length>0){lines.push("#","# Add the following variables (required by the current configuration):");for(const key of reconciliation.variablesToAdd)lines.push(`# - ${key}`);}return lines}async function writeEnvExample(contracts,location,fs,options={}){const onExisting=options.onExisting??"keep-sibling";let existingContent;try{existingContent=await fs.readFile(location,"utf8");}catch{}if(existingContent===void 0){const content2=renderEnvExample(contracts,[]);await fs.mkdir(path6__default.default.dirname(location),{recursive:true});await fs.writeFile(location,content2,"utf8");return {writtenPath:location,skippedExistingPath:void 0,staleVariables:[],variablesToComment:[],variablesToAdd:[]}}const reconciliation=computeReconciliation(contracts,existingContent);const{staleVariables,variablesToComment,variablesToAdd}=reconciliation;if(onExisting==="skip"){return {writtenPath:void 0,skippedExistingPath:location,staleVariables,variablesToComment,variablesToAdd}}const headerLines=onExisting==="keep-sibling"?renderReconciliationHeader(reconciliation):[];const content=renderEnvExample(contracts,headerLines);const writtenPath=onExisting==="overwrite"?location:`${location}.${Date.now()}`;const skippedExistingPath=onExisting==="overwrite"?void 0:location;await fs.mkdir(path6__default.default.dirname(writtenPath),{recursive:true});await fs.writeFile(writtenPath,content,"utf8");return {writtenPath,skippedExistingPath,staleVariables,variablesToComment,variablesToAdd}}function collectVariableNames(contracts){const names=new Set;for(const contract of contracts){for(const variable of contract.variables)names.add(variable.key);}return [...names]}function overrideVariable(variable,overrides){const override=overrides[variable.key];const date=override===void 0?void 0:parseIsoDate(override);return {...variable,expiresAt:date?override:variable.expiresAt,metadata:variable.metadata?{...variable.metadata}:variable.metadata}}function applyLiveExpirationOverrides(contracts,overrides){return contracts.map(contract=>({...contract,metadata:contract.metadata?{...contract.metadata}:contract.metadata,variables:contract.variables.map(variable=>overrideVariable(variable,overrides))}))}async function resolveLiveExpirationDates(contracts,liveExpirationDates){if(!liveExpirationDates)return contracts;const overrides=await liveExpirationDates(collectVariableNames(contracts));return deepFreeze(applyLiveExpirationOverrides(contracts,overrides))}var DEFAULT_EXPIRING_WITHIN_DAYS=30;var STANDARD_SENSITIVITY_LEVELS=new Set(["secret","credential","pii","config"]);function findNonstandardSensitivityLevels(contracts){const entries=[];for(const contract of contracts){if(contract.sensitivity!==void 0&&!STANDARD_SENSITIVITY_LEVELS.has(contract.sensitivity))entries.push({file:contract.file,exportName:contract.exportName,key:void 0,sensitivity:contract.sensitivity});for(const variable of [...contract.variables].sort((a,b)=>a.key.localeCompare(b.key))){if(variable.sensitivity===void 0)continue;if(STANDARD_SENSITIVITY_LEVELS.has(variable.sensitivity))continue;entries.push({file:contract.file,exportName:contract.exportName,key:variable.key,sensitivity:variable.sensitivity});}}return entries}function relativizeRef(root,ref){return {...ref,file:displayPath(root,ref.file)}}function computeDocumentation(root,linkResult,expiringWithinDays,generatedAt){const{contracts}=linkResult;return {contractSummaries:contracts.map(contract=>summarizeContract(contract,root)),catalog:buildCatalog(contracts),documentation:{undocumentedContracts:linkResult.undocumentedContracts,undocumentedVariables:linkResult.undocumentedVariables,staleDocEntries:linkResult.staleDocEntries,expiringSoon:computeExpiringEntries(contracts,expiringWithinDays,generatedAt),nonstandardSensitivityLevels:findNonstandardSensitivityLevels(contracts),unresolvedLinks:linkResult.unresolvedLinks},contractModelContracts:buildContractModel(contracts,root).contracts}}async function writeDocumentation(docsPath,envExamplePath,root,contracts,contractModelContracts,documentation,expiringWithinDays,generatedAt,fs,envExampleOnExisting){let previousContent;try{previousContent=await fs.readFile(docsPath,"utf8");}catch{}const docsSource=renderDocs(contractModelContracts,{expiringWithinDays,undocumentedContracts:documentation.undocumentedContracts.map(ref=>relativizeRef(root,ref)),undocumentedVariables:documentation.undocumentedVariables.map(ref=>relativizeRef(root,ref)),generatedAt,previousContent});await fs.mkdir(path6__default.default.dirname(docsPath),{recursive:true});await fs.writeFile(docsPath,docsSource,"utf8");const envExample=envExamplePath?await writeEnvExample(contracts,envExamplePath,fs,{onExisting:envExampleOnExisting}):void 0;return {envExample}}async function generateDocumentation(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const expiringWithinDays=options.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS;const docsLocationResult=resolveWithinRoot(root,options.location,"location","generateDocumentation");if(!docsLocationResult.ok)throw new EnvDocumentationGenerationError([docsLocationResult.issue]);const docsPath=docsLocationResult.resolved;let envExamplePath;if(options.envExample){const envExampleResult=resolveWithinRoot(root,options.envExample.location,"envExample.location","generateDocumentation");if(!envExampleResult.ok)throw new EnvDocumentationGenerationError([envExampleResult.issue]);envExamplePath=envExampleResult.resolved;}const{linkResult,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const docsContracts=await resolveLiveExpirationDates(linkResult.contracts,options.liveExpirationDates);const generatedAt=new Date;const computed=computeDocumentation(root,{...linkResult,contracts:docsContracts},expiringWithinDays,generatedAt);const{envExample}=await writeDocumentation(docsPath,envExamplePath,root,docsContracts,computed.contractModelContracts,computed.documentation,expiringWithinDays,generatedAt,options.fs,options.envExample?.onExisting);return {docsPath,envExample,contracts:computed.contractSummaries,catalog:computed.catalog,parseWarnings:[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],documentation:computed.documentation}}var LIFECYCLE_MODEL_SCHEMA_VERSION=2;function hasLifecycleData(variable){return variable.expiresAt!==void 0||variable.refreshInstructions!==void 0||variable.deprecated!==void 0||variable.removeBy!==void 0||variable.renamedFrom!==void 0||variable.retention!==void 0}function buildLifecycleModel(contracts,expiringWithinDays,now,root){const modelContracts=[];for(const contract of contracts){const variables=[...contract.variables].filter(hasLifecycleData).sort((a,b)=>a.key.localeCompare(b.key)).map(variable=>({key:variable.key,expiresAt:variable.expiresAt,refreshInstructions:variable.refreshInstructions,deprecated:variable.deprecated,deprecatedReason:variable.deprecatedReason,removeBy:variable.removeBy,renamedFrom:variable.renamedFrom,retention:variable.retention}));const hasContractLevelData=contract.expiresAt!==void 0||contract.deprecated!==void 0||contract.deprecatedReason!==void 0||contract.retention!==void 0;if(!hasContractLevelData&&variables.length===0)continue;modelContracts.push({file:displayPath(root,contract.file),exportName:contract.exportName,contractName:contract.contractName,expiresAt:contract.expiresAt,deprecated:contract.deprecated,deprecatedReason:contract.deprecatedReason,retention:contract.retention,variables});}modelContracts.sort(byContractIdentity);return {schemaVersion:LIFECYCLE_MODEL_SCHEMA_VERSION,contracts:modelContracts,expiring:computeExpiringEntries(contracts,expiringWithinDays,now).map(entry=>({...entry,file:displayPath(root,entry.file)}))}}var OWNERSHIP_MODEL_SCHEMA_VERSION=1;function buildOwnershipModel(contracts,root){const modelContracts=contracts.map(contract=>({file:displayPath(root,contract.file),exportName:contract.exportName,contractName:contract.contractName,owner:contract.owner,variables:[...contract.variables].sort((a,b)=>a.key.localeCompare(b.key)).map(variable=>({key:variable.key,owner:effectiveOwner(contract,variable)}))}));modelContracts.sort(byContractIdentity);const unownedContracts=modelContracts.filter(c=>c.owner===void 0).map(({file,exportName})=>({file,exportName}));const unownedVariables=[];for(const contract of modelContracts){for(const variable of contract.variables){if(variable.owner===void 0){unownedVariables.push({file:contract.file,exportName:contract.exportName,key:variable.key});}}}return {schemaVersion:OWNERSHIP_MODEL_SCHEMA_VERSION,contracts:modelContracts,unownedContracts,unownedVariables}}function readToolVersion(){return "0.2.0"}async function generateEvidenceModel(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const expiringWithinDays=options.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS;let snapshotPath;if(options.previousSnapshotLocation!==void 0){const result=resolveWithinRoot(root,options.previousSnapshotLocation,"previousSnapshotLocation","generateEvidenceModel");if(!result.ok)throw new EnvProjectGenerationError([result.issue]);snapshotPath=result.resolved;}const assembled=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const{readFileCached,linkResult,context,origins,packageWarnings,tsconfigWarnings}=assembled;const generatedAt=new Date;const activeContracts=linkResult.contracts.filter(c=>c.active);const lifecycleContracts=options.liveExpirationDates?await resolveLiveExpirationDates(linkResult.contracts,options.liveExpirationDates):linkResult.contracts;const contract=buildContractModel(linkResult.contracts,root);const ownership=buildOwnershipModel(linkResult.contracts,root);const lifecycle=buildLifecycleModel(lifecycleContracts,expiringWithinDays,generatedAt,root);const evidenceChanges=snapshotPath?await computeEvidenceChanges(root,snapshotPath,activeContracts,contract.contracts,readFileCached,options.fs):void 0;const change=buildChangeModel(evidenceChanges?.report??{addedContracts:[],removedContracts:[],addedVariables:[],removedVariables:[],updatedContracts:[],updatedVariables:[]},linkResult.contracts,root);const{scanFiles,scannedSurfaces}=await computeScanSurface(root,exclude,origins,options.fs);const dependency=await buildDependencyModel(linkResult.contracts,scanFiles,readFileCached,context,root,scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const documentationComputed=computeDocumentation(root,{...linkResult,contracts:lifecycleContracts},expiringWithinDays,generatedAt);const usageComputed=await computeUsage(root,linkResult.contracts,scanFiles,readFileCached,context,[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const finding=buildFindingModel({root,compatibilityIssues:detectCompatibilityIssues(activeContracts),exclusiveGroupIssues:detectExclusiveGroupIssues(activeContracts),documentation:documentationComputed.documentation,abandonedContracts:usageComputed.result.abandonedContracts,unresolvedConsumers:usageComputed.result.unresolvedConsumers,unconsumedOwnedVariables:usageComputed.result.unconsumedOwnedVariables,indeterminateOwnership:usageComputed.result.indeterminate,dynamicAccessCitationProblems:evidenceChanges?.dynamicAccessCitationProblems??[]});const commit=options.commit?await options.commit():void 0;return deepFreeze({schemaVersion:EVIDENCE_MODEL_SCHEMA_VERSION,provenance:{generatedAt:generatedAt.toISOString(),toolVersion:readToolVersion(),commit},contract,dependency,ownership,lifecycle,finding,change})}async function computeSourceFingerprint(options){const{fs,root,include,exclude,packages}=options;const localSchemaFiles=await discoverSchemaFiles({fs,root,include,exclude});const packageCache=new Map;const{files:packageFiles,origins}=await resolveAllowlistedPackages(packages,root,packageCache,fs);const schemaFiles=await mergeLocalAndPackageFiles(localSchemaFiles,packageFiles.map(f=>f.file),fs);const{scanFiles}=await computeScanSurface(root,exclude,origins,fs);const allFiles=[...new Set([...schemaFiles,...scanFiles])].sort();const hash=crypto__default.default.createHash("sha256");hash.update(readToolVersion());for(const file of allFiles){hash.update(displayPath(root,file));try{hash.update(await fs.readFile(file,"utf8"));}catch{hash.update("(unreadable)");}}return hash.digest("hex")}function fingerprintPathFor(evidencePath){return `${evidencePath}.fingerprint`}async function writeEvidenceFingerprint(evidencePath,fingerprint,fs){await fs.writeFile(fingerprintPathFor(evidencePath),`${fingerprint}
22
+ ${issues.length} ${noun} found:`}var EnvManifestGenerationError=class _EnvManifestGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Environment manifest generation failed.","incompatible declaration",issues),issues));this.code="ENV_MANIFEST_GENERATION_FAILED";this.name="EnvManifestGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvManifestGenerationError);}};var EnvDocumentationGenerationError=class _EnvDocumentationGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Environment documentation generation failed.","documentation issue",issues),issues));this.code="ENV_DOCUMENTATION_GENERATION_FAILED";this.name="EnvDocumentationGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvDocumentationGenerationError);}};var EnvUsageAnalysisError=class _EnvUsageAnalysisError extends Error{constructor(issues){super(formatIssues(countedHeader("Dependency ownership report generation failed.","ownership issue",issues),issues));this.code="ENV_USAGE_ANALYSIS_FAILED";this.name="EnvUsageAnalysisError";this.issues=issues;Error.captureStackTrace?.(this,_EnvUsageAnalysisError);}};var EnvProjectGenerationError=class _EnvProjectGenerationError extends Error{constructor(issues){super(formatIssues(countedHeader("Project generation failed.","blocking issue",issues),issues));this.code="ENV_PROJECT_GENERATION_FAILED";this.name="EnvProjectGenerationError";this.issues=issues;Error.captureStackTrace?.(this,_EnvProjectGenerationError);}};function detectExclusiveGroupIssues(contracts){const byGroup=new Map;for(const contract of contracts){if(!contract.active||!contract.exclusiveGroup)continue;const members=byGroup.get(contract.exclusiveGroup)??[];members.push(contract);byGroup.set(contract.exclusiveGroup,members);}const issues=[];for(const[group,members]of [...byGroup.entries()].sort((a,b)=>a[0].localeCompare(b[0]))){for(let i=0;i<members.length;i++){for(let j=i+1;j<members.length;j++){const a=members[i];const b=members[j];issues.push({severity:"error",variable:`Exclusive group "${group}"`,files:[a.file,b.file],reason:`"${a.contractName}" and "${b.contractName}" are both active and both declare exclusiveGroup "${group}" -- only one active contract per exclusive group is allowed. Set active: false on whichever one isn't in use.`});}}}return issues}function discoverValidationContexts(contracts){const contexts=new Set;for(const contract of contracts){if(!contract.active)continue;for(const variable of contract.variables){if(variable.context!==void 0)contexts.add(variable.context);}}return [...contexts].sort()}function renderManifest(contracts,outputFile){const sorted=[...contracts].sort((a,b)=>a.file.localeCompare(b.file));const outputDir=path6__default.default.dirname(outputFile);const usedNames=new Set;const importLines=[];const referenceNames=[];for(const contract of sorted){let localName=contract.exportName;let suffix=1;while(usedNames.has(localName)){localName=`${contract.exportName}_${suffix}`;suffix+=1;}usedNames.add(localName);referenceNames.push(localName);let specifier;if(contract.packageOrigin){specifier=contract.packageOrigin.packageName;}else {const withoutExt=contract.file.replace(/\.tsx?$/,"");specifier=path6__default.default.relative(outputDir,withoutExt).split(path6__default.default.sep).join("/");if(!specifier.startsWith("."))specifier=`./${specifier}`;}const binding=localName===contract.exportName?contract.exportName:`${contract.exportName} as ${localName}`;importLines.push(`import { ${binding} } from "${specifier}";`);}const activeContexts=discoverValidationContexts(contracts);const activeContextsLines=activeContexts.length>0?["// all currently active validation contexts",`export const activeContexts = [${activeContexts.map(c=>JSON.stringify(c)).join(", ")}];`,""]:[];const lines=[generatedBanner("ts"),"",...importLines,"",...activeContextsLines,"export const manifest = [",...referenceNames.map(name=>` ${name},`),"];",""];return lines.join("\n")}function defaultInclude(){return ["**/env.schema.ts"]}function defaultExclude(){return ["**/node_modules/**","**/dist/**","**/.git/**"]}function computeManifest(root,linkResult,onIncompatibility){const{contracts}=linkResult;const activeContracts=contracts.filter(contract=>contract.active);const compatibilityIssues=[...detectCompatibilityIssues(activeContracts),...detectExclusiveGroupIssues(activeContracts),...detectDuplicateVariableShapes(activeContracts)];const compatibilityErrors=compatibilityIssues.filter(issue=>issue.severity==="error");const blocking=onIncompatibility==="throw"?compatibilityIssues.filter(issue=>issue.severity!=="info"):compatibilityErrors;return {activeContracts,contractSummaries:contracts.map(contract=>summarizeContract(contract,root)),warnings:compatibilityIssues.filter(issue=>issue.severity==="warning"),blocking}}async function writeManifest(outputPath,activeContracts,fs){const manifestSource=renderManifest(activeContracts,outputPath);await fs.mkdir(path6__default.default.dirname(outputPath),{recursive:true});await fs.writeFile(outputPath,manifestSource,"utf8");}async function generateEnvManifest(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const onIncompatibility=options.onIncompatibility??"warn";const locationResult=resolveWithinRoot(root,options.location,"location","generateEnvManifest");if(!locationResult.ok)throw new EnvManifestGenerationError([locationResult.issue]);const outputPath=locationResult.resolved;const{linkResult,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const computed=computeManifest(root,linkResult,onIncompatibility);if(computed.blocking.length>0)throw new EnvManifestGenerationError(computed.blocking);await writeManifest(outputPath,computed.activeContracts,options.fs);return {outputPath,contracts:computed.contractSummaries,warnings:computed.warnings,parseWarnings:[...packageWarnings,...tsconfigWarnings,...linkResult.warnings]}}function sortedByContractName(entries){return [...entries].sort((a,b)=>a.contractName.localeCompare(b.contractName))}function renderDependencyOwnershipTable(entries){if(entries.length===0)return [];const lines=["## Dependency ownership","","Who owns each contract, who depends on it, and the blast radius if it changes.","","| Contract | Owner | Variables | Consumers | Blast radius |","|---|---|---|---|---|"];for(const entry of sortedByContractName(entries)){const consumers=entry.consumers.length>0?entry.consumers.join(", "):"--";lines.push(`| ${entry.contractName} (\`${entry.file}\`) | ${entry.owner??"--"} | ${entry.variableCount} | ${consumers} | ${entry.consumers.length} |`);}lines.push("");return lines}function renderAbandoned(findings){if(findings.length===0)return [];const lines=["## Abandoned ownership","","Contracts never imported anywhere in the scanned repository -- a feature's schema outliving the feature.","","| Contract | Owner | File |","|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| ${f.contractName} | ${f.owner??"--"} | \`${f.file}\` |`);lines.push("");return lines}function renderUnresolvedConsumers(findings){if(findings.length===0)return [];const lines=["## Unresolved consumers (barrel re-exports)","","Contracts reachable only through an unresolved `export * from` barrel re-export -- cannot be proven abandoned or consumed. Advisory only; never fails a build.",""];for(const f of sortedByContractName(findings))lines.push(`- **${f.contractName}** (\`${f.file}\`): ${f.reason}`);lines.push("");return lines}function formatStaleOrMissingCitations(citations){if(citations.length===0)return "--";return citations.map(c=>`${c.position.file}:${c.position.line}:${c.position.column} (${c.acknowledgment})`).join(", ")}function renderUnconsumedOwned(findings,scannedSurfaces){if(findings.length===0)return [];const surfaces=scannedSurfaces.map(s=>s.label).join(", ");const lines=["## Unconsumed owned dependencies","","Variables no consumer reads within the scanned surfaces below. Not proof of dead code. Common reasons a real consumer wouldn't show up here: it's read by a separate, out-of-repo service or webhook handler; it's consumed by non-TypeScript code (a shell script, a Dockerfile, a Terraform/Kubernetes manifest); or it's read from an allow-listed package whose directory wasn't included in this project's own `packages` configuration (ADR 0014).","",`Searched: ${surfaces}.`,"",'A blank Stale/missing citations cell is the strongest "looks genuinely unused" signal -- no developer has ever claimed otherwise. A non-blank cell means someone specifically claimed dynamic access here via `dynamicAccess`, and that claim can no longer be verified (ADR 0037) -- check with them before deleting.',"","| Variable | Contract | Owner | Stale/missing citations |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.owner??"--"} | ${formatStaleOrMissingCitations(f.staleOrMissingCitations)} |`);lines.push("");return lines}function renderIndeterminate(findings){if(findings.length===0)return [];const lines=["## Indeterminate (dynamic access)","","Dynamic (computed) property access was observed -- usage cannot be determined statically. Never guessed at.","","| Variable | Contract | Reason | Stale/missing citations |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.reason} | ${formatStaleOrMissingCitations(f.staleOrMissingCitations)} |`);lines.push("");return lines}function formatAssertions(assertions){const sites=assertions.map(a=>`${a.file}:${a.line}:${a.column}`).join(", ");return `Per developers, this data point is dynamically accessed at ${sites}.`}function renderAsserted(findings){if(findings.length===0)return [];const lines=["## Asserted (developer-acknowledged dynamic access)","","Variables env-cap's own scan would otherwise flag as unconsumed or indeterminate, but a developer has cited exactly where the dynamic access happens via `dynamicAccess`. The raw static status is shown alongside the citation, never hidden behind it -- a citation is a re-acknowledgment, not proof.","","| Variable | Contract | Static status | Developer assertion |","|---|---|---|---|"];for(const f of sortedByContractName(findings))lines.push(`| \`${f.key}\` | ${f.contractName} | ${f.wouldBeStatus} | ${formatAssertions(f.dynamicAccessAssertions)} |`);lines.push("");return lines}function renderParseWarnings(warnings){if(warnings.length===0)return [];const lines=["## Parse warnings","","Includes any `packages` (ADR 0014) resolution failures, alongside ordinary schema-discovery warnings.",""];for(const w of warnings)lines.push(`- **${w.file}**: ${w.message}`);lines.push("");return lines}function renderUsageReport(computed){const lines=[generatedBanner("markdown"),"",`> ${evidenceDisclaimer()}`,"",`> ${evidenceProjectionNote()}`,"","# Dependency & Ownership Report","","_Produced by `env-cap --ownership`._","","Which feature owns each variable, which features consume that contract, and what the blast radius is if it changes.","",...renderDependencyOwnershipTable(computed.dependencyOwnership),...renderAbandoned(computed.abandonedContracts),...renderUnresolvedConsumers(computed.unresolvedConsumers),...renderUnconsumedOwned(computed.unconsumedOwnedVariables,computed.scannedSurfaces),...renderIndeterminate(computed.indeterminate),...renderAsserted(computed.asserted),...renderParseWarnings(computed.parseWarnings)];return lines.join("\n").replace(/\n{3,}/g,"\n\n")}async function computeScanSurface(root,exclude,origins,fs){const localScanFiles=await discoverSchemaFiles({fs,root,include:["**/*.ts","**/*.tsx"],exclude});const scannedSurfaces=[{label:"application",root:"."}];const scanFiles=[...localScanFiles];const seenPackageDirs=new Set;for(const origin of origins.values()){if(seenPackageDirs.has(origin.packageDir))continue;seenPackageDirs.add(origin.packageDir);const packageScanFiles=await discoverSchemaFiles({fs,root:origin.packageDir,include:["**/*.ts","**/*.tsx"],exclude:defaultExclude()});scanFiles.push(...packageScanFiles);scannedSurfaces.push({label:`package:${origin.packageName}`,root:displayPath(root,origin.packageDir)});}return {scanFiles,scannedSurfaces}}async function computeUsage(root,contracts,scanFiles,readFile,context,parseWarnings,scannedSurfaces,dynamicAccessAcknowledgments){const graph=await buildDependencyGraph(contracts,scanFiles,readFile,context,scannedSurfaces,dynamicAccessAcknowledgments);const findings=deriveOwnershipFindings(graph);const contractByIdentity=new Map;for(const contract of contracts)contractByIdentity.set(`${contract.file}#${contract.exportName}`,contract);const ownerFor=(file,exportName)=>contractByIdentity.get(`${file}#${exportName}`)?.owner;const effectiveOwnerFor=(file,exportName,key)=>{const contract=contractByIdentity.get(`${file}#${exportName}`);if(!contract)return void 0;const variable=contract.variables.find(v=>v.key===key);return variable?effectiveOwner(contract,variable):contract.owner};const dependencyOwnership=graph.contracts.map(c=>({contractName:c.contractName,file:path6__default.default.relative(root,c.file),owner:ownerFor(c.file,c.exportName),variableCount:c.variables.size,consumers:c.consumingFiles.map(f=>path6__default.default.relative(root,f))}));const abandonedContracts=findings.abandoned.map(f=>({contractName:f.contractName,file:path6__default.default.relative(root,f.file),owner:ownerFor(f.file,f.exportName)}));const unresolvedConsumers=findings.unresolvedConsumers.map(f=>({contractName:f.contractName,file:path6__default.default.relative(root,f.file),reason:f.reason}));const staleOrMissingCitationsFor=(contractName,file,exportName,key)=>{const relativeFile=displayPath(root,file);const identity=dynamicAccessVariableIdentity(relativeFile,exportName,key);const problems=[];for(const a of dynamicAccessAcknowledgments?.get(identity)??[]){if(a.acknowledgment==="fresh")continue;problems.push({contractName,file:relativeFile,exportName,key,position:{file:a.file,line:a.line,column:a.column},acknowledgment:a.acknowledgment});}return problems};const unconsumedOwnedVariables=findings.unconsumedOwned.map(f=>({contractName:f.contractName,owner:effectiveOwnerFor(f.file,f.exportName,f.key),key:f.key,staleOrMissingCitations:staleOrMissingCitationsFor(f.contractName,f.file,f.exportName,f.key)}));const indeterminate=findings.indeterminate.map(f=>({contractName:f.contractName,key:f.key,reason:f.reason,dynamicAccessSites:f.dynamicAccessSites.map(p=>({...p,file:displayPath(root,p.file)})),staleOrMissingCitations:staleOrMissingCitationsFor(f.contractName,f.file,f.exportName,f.key)}));const asserted=findings.asserted.map(f=>({contractName:f.contractName,key:f.key,wouldBeStatus:f.wouldBeStatus,dynamicAccessAssertions:f.dynamicAccessAssertions}));return {graph,result:{dependencyOwnership,abandonedContracts,unresolvedConsumers,unconsumedOwnedVariables,indeterminate,asserted,parseWarnings,scannedSurfaces:graph.scannedSurfaces}}}async function writeUsageReport(reportPath,computed,fs){const source=renderUsageReport(computed);await fs.mkdir(path6__default.default.dirname(reportPath),{recursive:true});await fs.writeFile(reportPath,source,"utf8");}async function generateUsageReport(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];let reportPath;if(options.report){const result=resolveWithinRoot(root,options.report.location,"report.location","generateUsageReport");if(!result.ok)throw new EnvUsageAnalysisError([result.issue]);reportPath=result.resolved;}const{readFileCached,linkResult,context,origins,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const{scanFiles,scannedSurfaces}=await computeScanSurface(root,exclude,origins,options.fs);const computed=await computeUsage(root,linkResult.contracts,scanFiles,readFileCached,context,[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],scannedSurfaces);if(reportPath)await writeUsageReport(reportPath,computed.result,options.fs);return {reportPath,...computed.result}}var FINDING_MODEL_SCHEMA_VERSION=3;var NO_POSITION=void 0;function fromCompatibilityIssue(root,issue,code){return {severity:issue.severity,code,family:"compatibility",message:issue.reason,location:{model:"contract",file:displayPath(root,issue.files[0]),exportName:void 0,variable:issue.variable,position:NO_POSITION}}}function buildFindingModel(input){const{root}=input;const findings=[];for(const issue of input.compatibilityIssues??[]){findings.push(fromCompatibilityIssue(root,issue,issue.code??"DUPLICATE_VARIABLE_DOCUMENTATION"));}for(const issue of input.exclusiveGroupIssues??[]){findings.push(fromCompatibilityIssue(root,issue,"EXCLUSIVE_GROUP_VIOLATION"));}for(const check of input.artifactCheckFindings??[]){if(check.status==="ok")continue;findings.push({severity:"warning",code:check.status==="missing"?"ARTIFACT_MISSING":"ARTIFACT_STALE",family:"drift",message:check.detail??`${check.artifact} artifact is ${check.status}.`,location:{model:"change",path:check.path}});}const documentation=input.documentation;if(documentation){for(const c of documentation.undocumentedContracts){findings.push({severity:"warning",code:"UNDOCUMENTED_CONTRACT",family:"documentation",message:`"${c.exportName}" has no documentEnv() call linked to it.`,location:{model:"contract",file:displayPath(root,c.file),exportName:c.exportName,variable:void 0,position:NO_POSITION}});}for(const v of documentation.undocumentedVariables){findings.push({severity:"warning",code:"UNDOCUMENTED_VARIABLE",family:"documentation",message:`"${v.key}" (declared by "${v.exportName}") has no matching entry in a linked documentEnv()'s "variables".`,location:{model:"contract",file:displayPath(root,v.file),exportName:v.exportName,variable:v.key,position:NO_POSITION}});}for(const s of documentation.staleDocEntries){findings.push({severity:"warning",code:"STALE_DOC_ENTRY",family:"documentation",message:`"${s.key}" is documented under "${s.exportName}" but no longer exists in that contract's schema.`,location:{model:"contract",file:displayPath(root,s.file),exportName:s.exportName,variable:s.key,position:NO_POSITION}});}for(const e of documentation.expiringSoon){const expired=e.daysRemaining<0;findings.push({severity:"warning",code:expired?"EXPIRED":"EXPIRING_SOON",family:"documentation",message:expired?`"${e.key??e.exportName}" expired ${Math.abs(e.daysRemaining)} day(s) ago (expiresAt: ${e.expiresAt}).`:`"${e.key??e.exportName}" expires in ${e.daysRemaining} day(s) (expiresAt: ${e.expiresAt}).`,location:{model:"contract",file:displayPath(root,e.file),exportName:e.exportName,variable:e.key,position:NO_POSITION}});}for(const n of documentation.nonstandardSensitivityLevels){findings.push({severity:"info",code:"NONSTANDARD_SENSITIVITY_LEVEL",family:"documentation",message:`"${n.key??n.exportName}" declares sensitivity "${n.sensitivity}", which isn't one of the standard levels (secret/credential/pii/config) -- still honored verbatim, just flagged for vocabulary drift.`,location:{model:"contract",file:displayPath(root,n.file),exportName:n.exportName,variable:n.key,position:NO_POSITION}});}for(const u of documentation.unresolvedLinks){findings.push({severity:"warning",code:"UNRESOLVED_DOCUMENTENV_LINK",family:"documentation",message:u.reason,location:{model:"contract",file:displayPath(root,u.file),exportName:void 0,variable:void 0,position:NO_POSITION}});}}for(const a of input.abandonedContracts??[]){findings.push({severity:"warning",code:"ABANDONED_CONTRACT",family:"ownership",message:`"${a.contractName}" is never imported anywhere in the scanned repository.`,location:{model:"ownership",contractName:a.contractName,file:a.file,variable:void 0,position:NO_POSITION}});}for(const u of input.unresolvedConsumers??[]){findings.push({severity:"warning",code:"UNRESOLVED_CONSUMER",family:"ownership",message:u.reason,location:{model:"ownership",contractName:u.contractName,file:u.file,variable:void 0,position:NO_POSITION}});}for(const u of input.unconsumedOwnedVariables??[]){findings.push({severity:"warning",code:"UNCONSUMED_OWNED_VARIABLE",family:"ownership",message:`"${u.key}" (declared by "${u.contractName}") has no consumer found in the scanned repository.`,location:{model:"ownership",contractName:u.contractName,file:void 0,variable:u.key,position:NO_POSITION}});}for(const i of input.indeterminateOwnership??[]){findings.push({severity:"warning",code:"INDETERMINATE_OWNERSHIP",family:"ownership",message:i.reason,location:{model:"ownership",contractName:i.contractName,file:void 0,variable:i.key,position:NO_POSITION}});}for(const p of input.dynamicAccessCitationProblems??[]){const missing=p.acknowledgment==="missing";findings.push({severity:"warning",code:missing?"MISSING_DYNAMIC_ACCESS_CITATION":"STALE_DYNAMIC_ACCESS_CITATION",family:"ownership",message:missing?`"${p.key}" (declared by "${p.contractName}") cites dynamic access at ${p.position.file}:${p.position.line}:${p.position.column}, but that file no longer exists. Re-run generate:env once the citation is corrected.`:`"${p.key}" (declared by "${p.contractName}") cites dynamic access at ${p.position.file}:${p.position.line}:${p.position.column}, but that file's content has changed since it was last acknowledged. Re-run generate:env once you've re-confirmed the citation still applies.`,location:{model:"ownership",contractName:p.contractName,file:p.file,variable:p.key,position:p.position}});}return {schemaVersion:FINDING_MODEL_SCHEMA_VERSION,findings}}function humanizeKey(key){const spaced=key.replace(/([a-z0-9])([A-Z])/g,"$1 $2");return spaced.charAt(0).toUpperCase()+spaced.slice(1)}function renderMetadataValue(value){return typeof value==="string"?value:JSON.stringify(value)}function defineEvidenceProjection(schema){function project(evidence){const value={};const sources={};for(const key of Object.keys(schema)){const projector=schema[key];const{proxy,readPaths}=createTrackingProxy(evidence);value[key]=projector(proxy);sources[key]=readPaths();}return {value,sources}}function invoke(evidence){return project(evidence).value}return Object.assign(invoke,{project})}function readOnlyMembraneError(){return new TypeError("EvidenceModel is read-only inside a projector -- a projection must be a pure function of its evidence argument. See ADR 0032.")}function createTrackingProxy(evidence){const clone=structuredClone(evidence);const paths=new Set;const wrapped=new WeakMap;function wrap(value,path18){if(value===null||typeof value!=="object")return value;const target=value;const cached=wrapped.get(target);if(cached)return cached;const proxy=new Proxy(target,{get(currentTarget,prop,receiver){const result=Reflect.get(currentTarget,prop,receiver);if(typeof prop!=="string")return result;if(Array.isArray(currentTarget)&&prop==="length")return result;const nextPath=[...path18,prop];paths.add(nextPath.join("."));return wrap(result,nextPath)},set(){throw readOnlyMembraneError()},deleteProperty(){throw readOnlyMembraneError()},defineProperty(){throw readOnlyMembraneError()},setPrototypeOf(){throw readOnlyMembraneError()}});wrapped.set(target,proxy);return proxy}return {proxy:wrap(clone,[]),readPaths:()=>[...paths].sort()}}function byContractThenKey(a,b){return a.file.localeCompare(b.file)||a.exportName.localeCompare(b.exportName)||a.key.localeCompare(b.key)}function contractIndex(contracts){return new Map(contracts.map(c=>[`${c.file}#${c.exportName}`,c]))}function groupVariablesByOwner(contracts,ownerOf){const byOwner=new Map;for(const contract of contracts){for(const variable of contract.variables){const owner=ownerOf(contract,variable);if(owner===void 0)continue;const list=byOwner.get(owner)??[];list.push({contract,variable});byOwner.set(owner,list);}}return byOwner}var configurationReference=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),entries:evidence=>{const entries=[];for(const contract of evidence.contract.contracts){for(const variable of contract.variables){entries.push({file:contract.file,exportName:contract.exportName,contractName:contract.contractName,key:variable.key,description:variable.description,owner:variable.owner??contract.owner,sensitivity:variable.sensitivity??contract.sensitivity,required:variable.required,hasDefault:variable.hasDefault,hasProcessor:variable.hasProcessor,hasValidator:variable.hasValidator,expiresAt:variable.expiresAt,active:contract.active});}}return entries.sort(byContractThenKey)}});var ownershipSummary=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),owners:evidence=>{const contracts=contractIndex(evidence.contract.contracts);const nameOf=(file,exportName)=>contracts.get(`${file}#${exportName}`)?.contractName??exportName;const variablesByOwner=groupVariablesByOwner(evidence.ownership.contracts,(_contract,variable)=>variable.owner);const contractsByOwner=new Map;for(const contract of evidence.ownership.contracts){if(contract.owner===void 0)continue;const list=contractsByOwner.get(contract.owner)??[];list.push(nameOf(contract.file,contract.exportName));contractsByOwner.set(contract.owner,list);}const owners=new Set([...variablesByOwner.keys(),...contractsByOwner.keys()]);return [...owners].sort().map(owner=>({owner,variables:(variablesByOwner.get(owner)??[]).map(({contract,variable})=>`${nameOf(contract.file,contract.exportName)}.${variable.key}`).sort(),contracts:(contractsByOwner.get(owner)??[]).sort()}))},unowned:evidence=>{const contracts=contractIndex(evidence.contract.contracts);return evidence.ownership.unownedVariables.map(ref=>{const contractName=contracts.get(`${ref.file}#${ref.exportName}`)?.contractName??ref.exportName;return `${contractName}.${ref.key}`}).sort()}});var expiringSoonReport=defineEvidenceProjection({disclaimer:()=>evidenceDisclaimer(),entries:evidence=>{const contracts=contractIndex(evidence.contract.contracts);const ownerByVariable=new Map;for(const contract of evidence.ownership.contracts)for(const variable of contract.variables)ownerByVariable.set(`${contract.file}#${contract.exportName}#${variable.key}`,variable.owner);return evidence.lifecycle.expiring.map(entry=>{const contract=contracts.get(`${entry.file}#${entry.exportName}`);const variable=entry.key===void 0?void 0:contract?.variables.find(v=>v.key===entry.key);return {file:entry.file,exportName:entry.exportName,contractName:contract?.contractName??entry.exportName,key:entry.key,expiresAt:entry.expiresAt,daysRemaining:entry.daysRemaining,expired:entry.daysRemaining<0,refreshInstructions:variable?.refreshInstructions,owner:entry.key===void 0?contract?.owner:ownerByVariable.get(`${entry.file}#${entry.exportName}#${entry.key}`)??contract?.owner}})},expiredCount:evidence=>evidence.lifecycle.expiring.filter(e=>e.daysRemaining<0).length});function parseIsoDate(value){const date=new Date(value);return Number.isNaN(date.getTime())?void 0:date}var MS_PER_DAY=864e5;function daysRemainingFrom(date,now){return Math.ceil((date.getTime()-now.getTime())/MS_PER_DAY)}function computeExpiringEntries(contracts,expiringWithinDays,now){const entries=[];for(const contract of contracts){if(contract.expiresAt){const date=parseIsoDate(contract.expiresAt);if(date){const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<=expiringWithinDays){entries.push({file:contract.file,exportName:contract.exportName,key:void 0,expiresAt:contract.expiresAt,daysRemaining});}}}for(const variable of contract.variables){if(!variable.expiresAt)continue;const date=parseIsoDate(variable.expiresAt);if(!date)continue;const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<=expiringWithinDays){entries.push({file:contract.file,exportName:contract.exportName,key:variable.key,expiresAt:variable.expiresAt,daysRemaining});}}}return entries.sort((a,b)=>a.daysRemaining-b.daysRemaining)}function buildCatalog(contracts){return sortedContracts(contracts).map(contract=>{const variables={};for(const variable of contract.variables){variables[variable.key]={description:variable.description,owner:effectiveOwner(contract,variable),sensitivity:effectiveSensitivity(contract,variable),expiresAt:variable.expiresAt,refreshInstructions:variable.refreshInstructions,setupInstructions:variable.setupInstructions,required:variable.required,hasDefault:variable.hasDefault,hasProcessor:variable.hasProcessor,processorReturnType:variable.processorReturnType,hasValidator:variable.hasValidator,documented:variable.documented,context:variable.context,purpose:effectivePurpose(contract,variable),legalBasis:effectiveLegalBasis(contract,variable),retention:effectiveRetention(contract,variable),dataResidency:effectiveDataResidency(contract,variable),auditRequired:effectiveAuditRequired(contract,variable),metadata:variable.metadata,evidence:variable.evidence};}return {file:contract.file,exportName:contract.exportName,contractName:contract.contractName,active:contract.active,documented:contract.documented,category:contract.category,exclusiveGroup:contract.exclusiveGroup,...governanceFieldsOf(contract),variables}})}function extractPreviouslyDocumentedKeys(previousContent){const keys=new Set;for(const match of previousContent.matchAll(/^### `([A-Za-z_][A-Za-z0-9_]*)`$/gm)){if(match[1])keys.add(match[1]);}return keys}function extractPreviouslyActiveKeys(previousContent){const keys=new Set;let currentContractActive=false;for(const line of previousContent.split("\n")){const activeMatch=/^- Active: (yes|no)$/.exec(line);if(activeMatch){currentContractActive=activeMatch[1]==="yes";continue}const headingMatch=/^### `([A-Za-z_][A-Za-z0-9_]*)`$/.exec(line);if(headingMatch&&currentContractActive){keys.add(headingMatch[1]);}}return keys}function computeChangeSummary(contracts,previousContent){if(previousContent===void 0)return {added:[],removed:[],commented:[]};const previousKeys=extractPreviouslyDocumentedKeys(previousContent);const previousActiveKeys=extractPreviouslyActiveKeys(previousContent);const currentKeys=new Set;const currentActiveKeys=new Set;for(const contract of contracts){for(const variable of contract.variables){currentKeys.add(variable.key);if(contract.active)currentActiveKeys.add(variable.key);}}return {added:[...currentKeys].filter(k=>!previousKeys.has(k)).sort(),removed:[...previousKeys].filter(k=>!currentKeys.has(k)).sort(),commented:[...previousActiveKeys].filter(k=>currentKeys.has(k)&&!currentActiveKeys.has(k)).sort()}}function slugify(text){return text.toLowerCase().replace(/[^a-z0-9_]+/g,"-").replace(/^-+|-+$/g,"")||"section"}var AnchorRegistry=class{constructor(){this.used=new Map;this.assigned=new Map;}anchorFor(target,baseText){const existing=this.assigned.get(target);if(existing)return existing;const base=slugify(baseText);const count=this.used.get(base)??0;this.used.set(base,count+1);const anchor=count===0?base:`${base}-${count}`;this.assigned.set(target,anchor);return anchor}};function mdLink(text,anchor){return `[${text}](#${anchor})`}function sortedContracts(contracts){return [...contracts].sort((a,b)=>a.contractName.localeCompare(b.contractName))}function renderHeader(contracts,options,anchors,hasOwnership,hasDuplicates,hasLifecycle){const lines=[generatedBanner("markdown"),"",`> ${evidenceDisclaimer()}`,"",`> ${evidenceProjectionNote()}`,"","# Environment Variables","","_Produced by `env-cap --docs`._","",`_Generated ${options.generatedAt.toISOString()}_`,""];const summary=computeChangeSummary(contracts,options.previousContent);lines.push("## Changes since last report","");if(summary.added.length===0&&summary.removed.length===0&&summary.commented.length===0){lines.push("No changes.","");}else {if(summary.added.length>0)lines.push(`- **Added:** ${summary.added.map(k=>`\`${k}\``).join(", ")}`);if(summary.removed.length>0)lines.push(`- **Removed:** ${summary.removed.map(k=>`\`${k}\``).join(", ")}`);if(summary.commented.length>0)lines.push(`- **No longer required (inactive):** ${summary.commented.map(k=>`\`${k}\``).join(", ")}`);lines.push("");}lines.push("## Table of contents","",`- ${mdLink("Catalog","catalog")}`);for(const contract of sortedContracts(contracts)){lines.push(` - ${mdLink(contract.contractName,anchors.anchorFor(contract,`contract-${contract.contractName}`))}`);}if(hasOwnership)lines.push(`- ${mdLink("Ownership matrix","ownership-matrix")}`);if(hasDuplicates)lines.push(`- ${mdLink("Dependency graph","dependency-graph")}`);if(hasLifecycle)lines.push(`- ${mdLink("Lifecycle report","lifecycle-report")}`);lines.push(`- ${mdLink("Security review","security-review")}`,"");return lines}function renderCatalog(contracts,anchors,undocumented){const lines=["## Catalog","",'<a id="catalog"></a>',""];const undocumentedByIdentity=new Set(undocumented.map(u=>`${u.file}#${u.exportName}`));const usesValidationContexts=contracts.some(c=>c.variables.some(v=>v.context));if(usesValidationContexts){lines.push("> Validation contexts describe when validation participates. They do not restrict access to values, and they do not remove a variable's schema (or its `default` value) from wherever this manifest is imported.","");}for(const contract of sortedContracts(contracts)){lines.push(`<a id="${anchors.anchorFor(contract,`contract-${contract.contractName}`)}"></a>`);lines.push(`## ${contract.contractName}`,"",`Source: \`${contract.file}\``);if(undocumentedByIdentity.has(`${contract.file}#${contract.exportName}`)){lines.push("","> \u26A0\uFE0F **Undocumented.** No `documentEnv()` call is linked to this contract.");}lines.push(`- Active: ${contract.active?"yes":"no"}`);if(contract.category!==void 0)lines.push(`- Category: ${contract.category}`);if(contract.exclusiveGroup!==void 0)lines.push(`- Exclusive group: ${contract.exclusiveGroup}`);if(contract.owner!==void 0)lines.push(`- Owner: ${contract.owner}`);if(contract.sensitivity!==void 0)lines.push(`- Sensitivity: ${contract.sensitivity}`);if(contract.expiresAt!==void 0)lines.push(`- Expires: ${contract.expiresAt}`);if(contract.purpose!==void 0)lines.push(`- Purpose: ${contract.purpose}`);if(contract.legalBasis!==void 0)lines.push(`- Legal basis: ${contract.legalBasis}`);if(contract.retention!==void 0)lines.push(`- Retention: ${contract.retention}`);if(contract.dataResidency!==void 0)lines.push(`- Data residency: ${renderMetadataValue(contract.dataResidency)}`);if(contract.auditRequired!==void 0)lines.push(`- Audit required: ${contract.auditRequired?"yes":"no"}`);if(contract.metadata){for(const[metaKey,metaValue]of Object.entries(contract.metadata)){lines.push(`- ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}}lines.push("");for(const variable of [...contract.variables].sort((a,b)=>a.key<b.key?-1:1)){lines.push(`<a id="${anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`)}"></a>`);lines.push(`### \`${variable.key}\``,"");if(variable.description)lines.push(variable.description,"");if(!variable.documented)lines.push("> \u26A0\uFE0F **Undocumented.**","");lines.push(`- Default: ${variable.hasDefault?"yes":"no"}`,`- Processor: ${variable.hasProcessor?"yes":"no"}`,`- Validator: ${variable.hasValidator?"yes":"no"}`);if(variable.context!==void 0)lines.push(`- Validation context: ${variable.context}`);const owner=effectiveOwner(contract,variable);if(owner!==void 0)lines.push(`- Owner: ${owner}`);const sensitivity=effectiveSensitivity(contract,variable);if(sensitivity!==void 0)lines.push(`- Sensitivity: ${sensitivity}`);if(variable.expiresAt!==void 0)lines.push(`- Expires: ${variable.expiresAt}`);if(variable.setupInstructions!==void 0)lines.push(`- Setup instructions: ${variable.setupInstructions}`);if(variable.refreshInstructions!==void 0)lines.push(`- Refresh instructions: ${variable.refreshInstructions}`);if(variable.required!==void 0)lines.push(`- Required: ${variable.required?"yes":"no"}`);const purpose=effectivePurpose(contract,variable);if(purpose!==void 0)lines.push(`- Purpose: ${purpose}`);const legalBasis=effectiveLegalBasis(contract,variable);if(legalBasis!==void 0)lines.push(`- Legal basis: ${legalBasis}`);const retention=effectiveRetention(contract,variable);if(retention!==void 0)lines.push(`- Retention: ${retention}`);const dataResidency=effectiveDataResidency(contract,variable);if(dataResidency!==void 0)lines.push(`- Data residency: ${renderMetadataValue(dataResidency)}`);const auditRequired=effectiveAuditRequired(contract,variable);if(auditRequired!==void 0)lines.push(`- Audit required: ${auditRequired?"yes":"no"}`);const dynamicAccess=variable.evidence?.dynamicAccess;if(dynamicAccess&&dynamicAccess.length>0)lines.push(`- Dynamic access: ${dynamicAccess.join(", ")}`);for(const[metaKey,metaValue]of Object.entries(variable.metadata??{})){lines.push(`- ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}lines.push("");}}return lines}function renderOwnershipMatrix(contracts,anchors){const grouped=groupVariablesByOwner(sortedContracts(contracts),effectiveOwner);const byOwner=new Map;for(const[owner,entries]of grouped){byOwner.set(owner,entries.map(({contract,variable})=>mdLink(`\`${variable.key}\` (${contract.contractName})`,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))));}if(byOwner.size===0)return [];const lines=["## Ownership matrix","",'<a id="ownership-matrix"></a>',"","| Owner | Variables |","|---|---|"];for(const owner of [...byOwner.keys()].sort()){lines.push(`| ${owner} | ${mustGet(byOwner,owner).join(", ")} |`);}lines.push("");return lines}function renderDependencyGraph(contracts,anchors){const byKey=new Map;for(const contract of contracts){for(const variable of contract.variables){const list=byKey.get(variable.key)??[];list.push({contract,variable});byKey.set(variable.key,list);}}const hasDuplicates=[...byKey.values()].some(declarations=>declarations.length>1);const lines=["## Dependency graph","",'<a id="dependency-graph"></a>',"",`One row per unique variable name; more than one location means more than one feature declares it (see the package README's "Duplicate variables" section for how that's handled at runtime).`,"","| Variable | Declared in |","|---|---|"];for(const key of [...byKey.keys()].sort()){const declarations=mustGet(byKey,key);const locations=declarations.map(({contract,variable})=>mdLink(`${contract.contractName} (\`${contract.file}\`)`,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))).join(", ");lines.push(`| \`${key}\` | ${locations} |`);}lines.push("");return {lines,hasDuplicates}}function renderLifecycleReport(contracts,anchors,expiringWithinDays,now){const rows=[];for(const contract of sortedContracts(contracts)){for(const variable of contract.variables){const owner=effectiveOwner(contract,variable);if(variable.expiresAt===void 0&&owner===void 0&&variable.refreshInstructions===void 0)continue;const expiresAt=variable.expiresAt;const expiry=expiresAt?parseIsoDate(expiresAt):void 0;let expiresCell=expiresAt??"--";if(expiry&&expiresAt){const daysRemaining=daysRemainingFrom(expiry,now);if(daysRemaining<0)expiresCell=`${expiresAt} (**expired ${Math.abs(daysRemaining)}d ago**)`;else if(daysRemaining<=expiringWithinDays)expiresCell=`${expiresAt} (**${daysRemaining}d remaining**)`;}rows.push(`| ${mdLink(`\`${variable.key}\``,anchors.anchorFor(variable,`${contract.contractName}-${variable.key}`))} | ${owner??"--"} | ${expiresCell} | ${variable.refreshInstructions??"--"} |`);}}if(rows.length===0)return {lines:[],hasLifecycle:false};const lines=["## Lifecycle report","",'<a id="lifecycle-report"></a>',"","| Variable | Owner | Expires | Refresh instructions |","|---|---|---|---|",...rows,""];return {lines,hasLifecycle:true}}function computeSecurityReviewCounters(contracts,expiringWithinDays,now,undocumentedContractCount,undocumentedVariableCount){let totalVariableDeclarations=0;let activeVariableDeclarations=0;const uniqueKeys=new Set;const keyContractCount=new Map;let expiresAtSetCount=0;let expiredCount=0;let expiringSoonCount=0;let requiredCount=0;let refreshInstructionsCount=0;let noOwnerCount=0;for(const contract of contracts){for(const variable of contract.variables){totalVariableDeclarations+=1;if(contract.active)activeVariableDeclarations+=1;uniqueKeys.add(variable.key);keyContractCount.set(variable.key,(keyContractCount.get(variable.key)??0)+1);if(variable.required)requiredCount+=1;if(variable.refreshInstructions)refreshInstructionsCount+=1;if(effectiveOwner(contract,variable)===void 0)noOwnerCount+=1;if(variable.expiresAt){expiresAtSetCount+=1;const date=parseIsoDate(variable.expiresAt);if(date){const daysRemaining=daysRemainingFrom(date,now);if(daysRemaining<0)expiredCount+=1;else if(daysRemaining<=expiringWithinDays)expiringSoonCount+=1;}}}}const duplicateVariableNameCount=[...keyContractCount.values()].filter(count=>count>1).length;return {totalContracts:contracts.length,totalVariableDeclarations,activeVariableDeclarations,uniqueVariableNames:uniqueKeys.size,expiresAtSetCount,expiredCount,expiringSoonCount,requiredCount,refreshInstructionsCount,noOwnerCount,duplicateVariableNameCount,undocumentedContractCount,undocumentedVariableCount}}function renderSecurityReview(contracts,options,now){const counters=computeSecurityReviewCounters(contracts,options.expiringWithinDays,now,options.undocumentedContracts.length,options.undocumentedVariables.length);return ["## Security review","",'<a id="security-review"></a>',"",`- Total contracts: ${counters.totalContracts}`,`- Total variable declarations: ${counters.totalVariableDeclarations} (${counters.activeVariableDeclarations} from active contracts)`,`- Unique variable names: ${counters.uniqueVariableNames}`,`- Variables with \`expiresAt\` set: ${counters.expiresAtSetCount}`,` - Already expired: ${counters.expiredCount}`,` - Expiring within ${options.expiringWithinDays} days: ${counters.expiringSoonCount}`,`- Variables marked \`required: true\`: ${counters.requiredCount}`,`- Variables with refresh instructions: ${counters.refreshInstructionsCount}`,`- Variables with no assigned owner: ${counters.noOwnerCount}`,`- Variable names declared by more than one contract: ${counters.duplicateVariableNameCount}`,`- Undocumented contracts: ${counters.undocumentedContractCount}`,`- Undocumented variables: ${counters.undocumentedVariableCount}`,""]}function renderDocs(contracts,options){const anchors=new AnchorRegistry;const sorted=sortedContracts(contracts);const ownership=renderOwnershipMatrix(sorted,anchors);const{lines:dependencyLines,hasDuplicates}=renderDependencyGraph(sorted,anchors);const{lines:lifecycleLines,hasLifecycle}=renderLifecycleReport(sorted,anchors,options.expiringWithinDays,options.generatedAt);const header=renderHeader(sorted,options,anchors,ownership.length>0,hasDuplicates,hasLifecycle);const catalog=renderCatalog(sorted,anchors,options.undocumentedContracts);const security=renderSecurityReview(sorted,options,options.generatedAt);return [...header,...catalog,...ownership,...dependencyLines,...lifecycleLines,...security].join("\n").replace(/\n{3,}/g,"\n\n")}function normalizeDocsForComparison(content){return content.replace(/^_Generated .+_$/m,"_Generated <normalized-for-comparison>_").replace(/\(\*\*\d+d remaining\*\*\)/g,"(**Nd remaining**)").replace(/\(\*\*expired \d+d ago\*\*\)/g,"(**expired Nd ago**)").replace(/^(- Already expired:) \d+$/gm,"$1 N").replace(/^(- Expiring within \d+ days:) \d+$/gm,"$1 N")}function isPrimitiveLiteral(value){return typeof value==="string"||typeof value==="number"||typeof value==="boolean"}function defaultLiteralFor(variable){return variable.defaultValue?.ok===true&&isPrimitiveLiteral(variable.defaultValue.value)?String(variable.defaultValue.value):void 0}function renderVariableLines(variable,options={}){const lines=[];if(options.note)lines.push(`# ${options.note}`);if(variable.description)lines.push(`# ${variable.description}`);if(variable.context)lines.push(`# Validation context: ${variable.context}`);if(variable.owner)lines.push(`# Owner: ${variable.owner}`);if(variable.setupInstructions)lines.push(`# Setup: ${variable.setupInstructions}`);if(variable.expiresAt)lines.push(`# Expires At: ${variable.expiresAt}`);if(variable.refreshInstructions)lines.push(`# Refresh Instructions: ${variable.refreshInstructions}`);if(variable.required)lines.push(`# Required: yes`);if(variable.purpose)lines.push(`# Purpose: ${variable.purpose}`);if(variable.legalBasis)lines.push(`# Legal Basis: ${variable.legalBasis}`);if(variable.retention)lines.push(`# Retention Policy: ${variable.retention}`);if(variable.dataResidency)lines.push(`# Data Residency: ${renderMetadataValue(variable.dataResidency)}`);if(variable.auditRequired)lines.push(`# Audit Required: yes`);for(const[metaKey,metaValue]of Object.entries(variable.metadata??{})){lines.push(`# ${humanizeKey(metaKey)}: ${renderMetadataValue(metaValue)}`);}const prefix=options.commented?"# ":"";lines.push(`${prefix}${variable.key}=${defaultLiteralFor(variable)??""}`,"");return lines}function groupByKey(contracts){const byKey=new Map;for(const contract of contracts){for(const variable of contract.variables){const list=byKey.get(variable.key)??[];list.push({contract,variable});byKey.set(variable.key,list);}}return byKey}function renderEnvExample(contracts,reconciliationHeader=[]){const sorted=[...contracts].sort((a,b)=>a.file.localeCompare(b.file));const active=sorted.filter(c=>c.active);const inactive=sorted.filter(c=>!c.active);const lines=["# AUTO-GENERATED EXAMPLE FILE.","# Copy to .env and fill in real values. Do not commit .env."];if(sorted.some(c=>c.variables.some(v=>v.context))){lines.push("# Validation context annotations describe when validation participates.","# They do not restrict access to values, and every variable below is","# still written to this file regardless of its context.");}if(reconciliationHeader.length>0)lines.push(...reconciliationHeader);lines.push("");const activeByKey=groupByKey(active);for(const key of [...activeByKey.keys()].sort()){const[first,...rest]=mustGet(activeByKey,key);lines.push(...renderVariableLines(first.variable));for(const dup of rest){lines.push(...renderVariableLines(dup.variable,{commented:true,note:`Also declared by "${dup.contract.contractName}" -- see "${first.contract.contractName}" above.`}));}}const inactiveByKey=groupByKey(inactive);for(const key of [...inactiveByKey.keys()].sort()){if(activeByKey.has(key))continue;const[first]=mustGet(inactiveByKey,key);lines.push(...renderVariableLines(first.variable,{commented:true,note:`Disabled -- feature "${first.contract.contractName}" is not active.`}));}return lines.join("\n").replace(/\n+$/,"\n")}function extractDeclaredVariables(source){const names=[];for(const line of source.split("\n")){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;const match=/^([A-Za-z_][A-Za-z0-9_]*)=/.exec(trimmed);if(match?.[1])names.push(match[1]);}return names}function extractCommentedVariables(source){const names=[];for(const line of source.split("\n")){const trimmed=line.trim();if(!trimmed.startsWith("#"))continue;const withoutHash=trimmed.replace(/^#+\s*/,"");const match=/^([A-Za-z_][A-Za-z0-9_]*)=/.exec(withoutHash);if(match?.[1])names.push(match[1]);}return names}function computeReconciliation(contracts,existingContent){const activeKeys=new Set(contracts.filter(c=>c.active).flatMap(c=>c.variables.map(v=>v.key)));const knownKeys=new Set(contracts.flatMap(c=>c.variables.map(v=>v.key)));const oldLive=new Set(extractDeclaredVariables(existingContent));const oldKnown=new Set([...oldLive,...extractCommentedVariables(existingContent)]);const staleVariables=[...oldKnown].filter(key=>!knownKeys.has(key)).sort();const variablesToComment=[...oldLive].filter(key=>!activeKeys.has(key)&&knownKeys.has(key)).sort();const variablesToAdd=[...activeKeys].filter(key=>!oldLive.has(key)).sort();return {staleVariables,variablesToComment,variablesToAdd}}function renderReconciliationHeader(reconciliation){const lines=[];if(reconciliation.staleVariables.length>0){lines.push("#","# Remove the following variables (no longer declared by any feature):");for(const key of reconciliation.staleVariables)lines.push(`# - ${key}`);}if(reconciliation.variablesToComment.length>0){lines.push("#","# Comment the following variables (their feature is no longer active):");for(const key of reconciliation.variablesToComment)lines.push(`# - ${key}`);}if(reconciliation.variablesToAdd.length>0){lines.push("#","# Add the following variables (required by the current configuration):");for(const key of reconciliation.variablesToAdd)lines.push(`# - ${key}`);}return lines}async function writeEnvExample(contracts,location,fs,options={}){const onExisting=options.onExisting??"keep-sibling";let existingContent;try{existingContent=await fs.readFile(location,"utf8");}catch{}if(existingContent===void 0){const content2=renderEnvExample(contracts,[]);await fs.mkdir(path6__default.default.dirname(location),{recursive:true});await fs.writeFile(location,content2,"utf8");return {writtenPath:location,skippedExistingPath:void 0,staleVariables:[],variablesToComment:[],variablesToAdd:[]}}const reconciliation=computeReconciliation(contracts,existingContent);const{staleVariables,variablesToComment,variablesToAdd}=reconciliation;if(onExisting==="skip"){return {writtenPath:void 0,skippedExistingPath:location,staleVariables,variablesToComment,variablesToAdd}}const headerLines=onExisting==="keep-sibling"?renderReconciliationHeader(reconciliation):[];const content=renderEnvExample(contracts,headerLines);const writtenPath=onExisting==="overwrite"?location:`${location}.${Date.now()}`;const skippedExistingPath=onExisting==="overwrite"?void 0:location;await fs.mkdir(path6__default.default.dirname(writtenPath),{recursive:true});await fs.writeFile(writtenPath,content,"utf8");return {writtenPath,skippedExistingPath,staleVariables,variablesToComment,variablesToAdd}}function collectVariableNames(contracts){const names=new Set;for(const contract of contracts){for(const variable of contract.variables)names.add(variable.key);}return [...names]}function overrideVariable(variable,overrides){const override=overrides[variable.key];const date=override===void 0?void 0:parseIsoDate(override);return {...variable,expiresAt:date?override:variable.expiresAt,metadata:variable.metadata?{...variable.metadata}:variable.metadata}}function applyLiveExpirationOverrides(contracts,overrides){return contracts.map(contract=>({...contract,metadata:contract.metadata?{...contract.metadata}:contract.metadata,variables:contract.variables.map(variable=>overrideVariable(variable,overrides))}))}async function resolveLiveExpirationDates(contracts,liveExpirationDates){if(!liveExpirationDates)return contracts;const overrides=await liveExpirationDates(collectVariableNames(contracts));return deepFreeze(applyLiveExpirationOverrides(contracts,overrides))}var DEFAULT_EXPIRING_WITHIN_DAYS=30;var STANDARD_SENSITIVITY_LEVELS=new Set(["secret","credential","pii","config"]);function findNonstandardSensitivityLevels(contracts){const entries=[];for(const contract of contracts){if(contract.sensitivity!==void 0&&!STANDARD_SENSITIVITY_LEVELS.has(contract.sensitivity))entries.push({file:contract.file,exportName:contract.exportName,key:void 0,sensitivity:contract.sensitivity});for(const variable of [...contract.variables].sort((a,b)=>a.key.localeCompare(b.key))){if(variable.sensitivity===void 0)continue;if(STANDARD_SENSITIVITY_LEVELS.has(variable.sensitivity))continue;entries.push({file:contract.file,exportName:contract.exportName,key:variable.key,sensitivity:variable.sensitivity});}}return entries}function relativizeRef(root,ref){return {...ref,file:displayPath(root,ref.file)}}function computeDocumentation(root,linkResult,expiringWithinDays,generatedAt){const{contracts}=linkResult;return {contractSummaries:contracts.map(contract=>summarizeContract(contract,root)),catalog:buildCatalog(contracts),documentation:{undocumentedContracts:linkResult.undocumentedContracts,undocumentedVariables:linkResult.undocumentedVariables,staleDocEntries:linkResult.staleDocEntries,expiringSoon:computeExpiringEntries(contracts,expiringWithinDays,generatedAt),nonstandardSensitivityLevels:findNonstandardSensitivityLevels(contracts),unresolvedLinks:linkResult.unresolvedLinks},contractModelContracts:buildContractModel(contracts,root).contracts}}async function writeDocumentation(docsPath,envExamplePath,root,contracts,contractModelContracts,documentation,expiringWithinDays,generatedAt,fs,envExampleOnExisting){let previousContent;try{previousContent=await fs.readFile(docsPath,"utf8");}catch{}const docsSource=renderDocs(contractModelContracts,{expiringWithinDays,undocumentedContracts:documentation.undocumentedContracts.map(ref=>relativizeRef(root,ref)),undocumentedVariables:documentation.undocumentedVariables.map(ref=>relativizeRef(root,ref)),generatedAt,previousContent});await fs.mkdir(path6__default.default.dirname(docsPath),{recursive:true});await fs.writeFile(docsPath,docsSource,"utf8");const envExample=envExamplePath?await writeEnvExample(contracts,envExamplePath,fs,{onExisting:envExampleOnExisting}):void 0;return {envExample}}async function generateDocumentation(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const expiringWithinDays=options.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS;const docsLocationResult=resolveWithinRoot(root,options.location,"location","generateDocumentation");if(!docsLocationResult.ok)throw new EnvDocumentationGenerationError([docsLocationResult.issue]);const docsPath=docsLocationResult.resolved;let envExamplePath;if(options.envExample){const envExampleResult=resolveWithinRoot(root,options.envExample.location,"envExample.location","generateDocumentation");if(!envExampleResult.ok)throw new EnvDocumentationGenerationError([envExampleResult.issue]);envExamplePath=envExampleResult.resolved;}const{linkResult,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const docsContracts=await resolveLiveExpirationDates(linkResult.contracts,options.liveExpirationDates);const generatedAt=new Date;const computed=computeDocumentation(root,{...linkResult,contracts:docsContracts},expiringWithinDays,generatedAt);const{envExample}=await writeDocumentation(docsPath,envExamplePath,root,docsContracts,computed.contractModelContracts,computed.documentation,expiringWithinDays,generatedAt,options.fs,options.envExample?.onExisting);return {docsPath,envExample,contracts:computed.contractSummaries,catalog:computed.catalog,parseWarnings:[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],documentation:computed.documentation}}var LIFECYCLE_MODEL_SCHEMA_VERSION=2;function hasLifecycleData(variable){return variable.expiresAt!==void 0||variable.refreshInstructions!==void 0||variable.deprecated!==void 0||variable.removeBy!==void 0||variable.renamedFrom!==void 0||variable.retention!==void 0}function buildLifecycleModel(contracts,expiringWithinDays,now,root){const modelContracts=[];for(const contract of contracts){const variables=[...contract.variables].filter(hasLifecycleData).sort((a,b)=>a.key.localeCompare(b.key)).map(variable=>({key:variable.key,expiresAt:variable.expiresAt,refreshInstructions:variable.refreshInstructions,deprecated:variable.deprecated,deprecatedReason:variable.deprecatedReason,removeBy:variable.removeBy,renamedFrom:variable.renamedFrom,retention:variable.retention}));const hasContractLevelData=contract.expiresAt!==void 0||contract.deprecated!==void 0||contract.deprecatedReason!==void 0||contract.retention!==void 0;if(!hasContractLevelData&&variables.length===0)continue;modelContracts.push({file:displayPath(root,contract.file),exportName:contract.exportName,contractName:contract.contractName,expiresAt:contract.expiresAt,deprecated:contract.deprecated,deprecatedReason:contract.deprecatedReason,retention:contract.retention,variables});}modelContracts.sort(byContractIdentity);return {schemaVersion:LIFECYCLE_MODEL_SCHEMA_VERSION,contracts:modelContracts,expiring:computeExpiringEntries(contracts,expiringWithinDays,now).map(entry=>({...entry,file:displayPath(root,entry.file)}))}}var OWNERSHIP_MODEL_SCHEMA_VERSION=1;function buildOwnershipModel(contracts,root){const modelContracts=contracts.map(contract=>({file:displayPath(root,contract.file),exportName:contract.exportName,contractName:contract.contractName,owner:contract.owner,variables:[...contract.variables].sort((a,b)=>a.key.localeCompare(b.key)).map(variable=>({key:variable.key,owner:effectiveOwner(contract,variable)}))}));modelContracts.sort(byContractIdentity);const unownedContracts=modelContracts.filter(c=>c.owner===void 0).map(({file,exportName})=>({file,exportName}));const unownedVariables=[];for(const contract of modelContracts){for(const variable of contract.variables){if(variable.owner===void 0){unownedVariables.push({file:contract.file,exportName:contract.exportName,key:variable.key});}}}return {schemaVersion:OWNERSHIP_MODEL_SCHEMA_VERSION,contracts:modelContracts,unownedContracts,unownedVariables}}function readToolVersion(){return "0.3.0"}async function generateEvidenceModel(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const expiringWithinDays=options.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS;let snapshotPath;if(options.previousSnapshotLocation!==void 0){const result=resolveWithinRoot(root,options.previousSnapshotLocation,"previousSnapshotLocation","generateEvidenceModel");if(!result.ok)throw new EnvProjectGenerationError([result.issue]);snapshotPath=result.resolved;}const assembled=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const{readFileCached,linkResult,context,origins,packageWarnings,tsconfigWarnings}=assembled;const generatedAt=new Date;const activeContracts=linkResult.contracts.filter(c=>c.active);const lifecycleContracts=options.liveExpirationDates?await resolveLiveExpirationDates(linkResult.contracts,options.liveExpirationDates):linkResult.contracts;const contract=buildContractModel(linkResult.contracts,root);const ownership=buildOwnershipModel(linkResult.contracts,root);const lifecycle=buildLifecycleModel(lifecycleContracts,expiringWithinDays,generatedAt,root);const evidenceChanges=snapshotPath?await computeEvidenceChanges(root,snapshotPath,activeContracts,contract.contracts,readFileCached,options.fs):void 0;const change=buildChangeModel(evidenceChanges?.report??{addedContracts:[],removedContracts:[],addedVariables:[],removedVariables:[],updatedContracts:[],updatedVariables:[]},linkResult.contracts,root);const{scanFiles,scannedSurfaces}=await computeScanSurface(root,exclude,origins,options.fs);const dependency=await buildDependencyModel(linkResult.contracts,scanFiles,readFileCached,context,root,scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const documentationComputed=computeDocumentation(root,{...linkResult,contracts:lifecycleContracts},expiringWithinDays,generatedAt);const usageComputed=await computeUsage(root,linkResult.contracts,scanFiles,readFileCached,context,[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const finding=buildFindingModel({root,compatibilityIssues:detectCompatibilityIssues(activeContracts),exclusiveGroupIssues:detectExclusiveGroupIssues(activeContracts),documentation:documentationComputed.documentation,abandonedContracts:usageComputed.result.abandonedContracts,unresolvedConsumers:usageComputed.result.unresolvedConsumers,unconsumedOwnedVariables:usageComputed.result.unconsumedOwnedVariables,indeterminateOwnership:usageComputed.result.indeterminate,dynamicAccessCitationProblems:evidenceChanges?.dynamicAccessCitationProblems??[]});const commit=options.commit?await options.commit():void 0;return deepFreeze({schemaVersion:EVIDENCE_MODEL_SCHEMA_VERSION,provenance:{generatedAt:generatedAt.toISOString(),toolVersion:readToolVersion(),commit},contract,dependency,ownership,lifecycle,finding,change})}async function computeSourceFingerprint(options){const{fs,root,include,exclude,packages}=options;const localSchemaFiles=await discoverSchemaFiles({fs,root,include,exclude});const packageCache=new Map;const{files:packageFiles,origins}=await resolveAllowlistedPackages(packages,root,packageCache,fs);const schemaFiles=await mergeLocalAndPackageFiles(localSchemaFiles,packageFiles.map(f=>f.file),fs);const{scanFiles}=await computeScanSurface(root,exclude,origins,fs);const allFiles=[...new Set([...schemaFiles,...scanFiles])].sort();const hash=crypto__default.default.createHash("sha256");hash.update(readToolVersion());for(const file of allFiles){hash.update(displayPath(root,file));try{hash.update(await fs.readFile(file,"utf8"));}catch{hash.update("(unreadable)");}}return hash.digest("hex")}function fingerprintPathFor(evidencePath){return `${evidencePath}.fingerprint`}async function writeEvidenceFingerprint(evidencePath,fingerprint,fs){await fs.writeFile(fingerprintPathFor(evidencePath),`${fingerprint}
23
23
  `,"utf8");}async function getEvidenceModel(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const evidencePath=path6__default.default.resolve(root,options.location);const fingerprintPath=fingerprintPathFor(evidencePath);const recompute=async missReason=>({evidence:await generateEvidenceModel(options),source:"miss",missReason});let currentFingerprint;try{currentFingerprint=await computeSourceFingerprint({fs:options.fs,root,include,exclude,packages});}catch(error){return recompute(`could not compute a source fingerprint (${error instanceof Error?error.message:String(error)})`)}let storedFingerprint;try{storedFingerprint=(await options.fs.readFile(fingerprintPath,"utf8")).trim();}catch{return recompute(`no fingerprint sidecar found at ${fingerprintPath}`)}if(storedFingerprint!==currentFingerprint){return recompute("source fingerprint changed since the committed evidence artifact was last generated")}const read=await readEvidenceSnapshot(evidencePath,options.fs);if(read.status!=="ok"){return recompute(read.status==="missing"?`no evidence artifact found at ${evidencePath}`:read.status==="invalid-json"?`evidence artifact at ${evidencePath} could not be parsed as JSON (${read.detail})`:`evidence artifact at ${evidencePath} has an unrecognized schemaVersion (${JSON.stringify(read.foundVersion)})`)}return {evidence:read.snapshot,source:"hit",missReason:void 0}}function escalatedFindings(findings,family){return findings.filter(f=>f.family===family&&f.severity==="warning").map(f=>({severity:"error",variable:findingSubject(f),files:findingFiles(f),reason:`[${f.code}] ${f.message}`}))}function findingSubject(finding){const location=finding.location;if(location.model==="change")return `(artifact) ${location.path}`;if(location.variable!==void 0)return location.variable;const name=location.model==="ownership"?location.contractName:location.exportName;return `(contract) ${name??"unknown"}`}function findingFiles(finding){const location=finding.location;if(location.model==="change")return [location.path];return location.file===void 0?[]:[location.file]}async function computeArtifacts(options){const root=path6__default.default.resolve(options.root??process.cwd());const include=options.include??defaultInclude();const exclude=options.exclude??defaultExclude();const packages=options.packages??[];const manifestOptions=options.manifest===false?void 0:options.manifest;const docsOptions=options.docs===false?void 0:options.docs;const usageOptions=options.usage===false?void 0:options.usage;const evidenceOptions=options.evidence===false?void 0:options.evidence;const pathIssues=[];let manifestOutputPath;if(manifestOptions){const result=resolveWithinRoot(root,manifestOptions.location,"manifest.location","generateEnvArtifacts");if(result.ok)manifestOutputPath=result.resolved;else pathIssues.push(result.issue);}let docsPath;let envExamplePath;if(docsOptions){const result=resolveWithinRoot(root,docsOptions.location,"docs.location","generateEnvArtifacts");if(result.ok)docsPath=result.resolved;else pathIssues.push(result.issue);if(docsOptions.envExample){const exampleResult=resolveWithinRoot(root,docsOptions.envExample.location,"docs.envExample.location","generateEnvArtifacts");if(exampleResult.ok)envExamplePath=exampleResult.resolved;else pathIssues.push(exampleResult.issue);}}let usageReportPath;if(usageOptions?.report){const result=resolveWithinRoot(root,usageOptions.report.location,"usage.report.location","generateEnvArtifacts");if(result.ok)usageReportPath=result.resolved;else pathIssues.push(result.issue);}let evidencePath;if(evidenceOptions){const result=resolveWithinRoot(root,evidenceOptions.location,"evidence.location","generateEnvArtifacts");if(result.ok)evidencePath=result.resolved;else pathIssues.push(result.issue);}if(pathIssues.length>0)throw new EnvProjectGenerationError(pathIssues);const{readFileCached,linkResult,context,origins,packageWarnings,tsconfigWarnings}=await assembleProject({fs:options.fs,root,include,exclude,packages,tsconfig:options.tsconfig});const generatedAt=new Date;const activeContracts=linkResult.contracts.filter(c=>c.active);const liveExpirationContracts=options.liveExpirationDates?await resolveLiveExpirationDates(linkResult.contracts,options.liveExpirationDates):linkResult.contracts;const docsContracts=docsOptions?liveExpirationContracts:linkResult.contracts;const blocking=[];const manifestComputed=manifestOptions?computeManifest(root,linkResult,manifestOptions.onIncompatibility??"warn"):void 0;if(manifestComputed)blocking.push(...manifestComputed.blocking);const docsComputed=computeDocumentation(root,{...linkResult,contracts:liveExpirationContracts},docsOptions?.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS,generatedAt);const{scanFiles,scannedSurfaces}=await computeScanSurface(root,exclude,origins,options.fs);const contract=buildContractModel(linkResult.contracts,root);const evidenceChanges=evidencePath?await computeEvidenceChanges(root,evidencePath,activeContracts,contract.contracts,readFileCached,options.fs):void 0;const usageComputed=await computeUsage(root,linkResult.contracts,scanFiles,readFileCached,context,[...packageWarnings,...tsconfigWarnings,...linkResult.warnings],scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const ownership=buildOwnershipModel(linkResult.contracts,root);const lifecycle=buildLifecycleModel(liveExpirationContracts,docsOptions?.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS,generatedAt,root);const change=buildChangeModel(evidenceChanges?.report??{addedContracts:[],removedContracts:[],addedVariables:[],removedVariables:[],updatedContracts:[],updatedVariables:[]},linkResult.contracts,root);const dependency=await buildDependencyModel(linkResult.contracts,scanFiles,readFileCached,context,root,scannedSurfaces,evidenceChanges?.dynamicAccessAcknowledgments);const finding=buildFindingModel({root,compatibilityIssues:[...detectCompatibilityIssues(activeContracts),...detectDuplicateVariableShapes(activeContracts)],exclusiveGroupIssues:detectExclusiveGroupIssues(activeContracts),documentation:docsComputed.documentation,abandonedContracts:usageComputed.result.abandonedContracts,unresolvedConsumers:usageComputed.result.unresolvedConsumers,unconsumedOwnedVariables:usageComputed.result.unconsumedOwnedVariables,indeterminateOwnership:usageComputed.result.indeterminate,dynamicAccessCitationProblems:evidenceChanges?.dynamicAccessCitationProblems??[]});if(options.onUndocumented==="throw")blocking.push(...escalatedFindings(finding.findings,"documentation"));if(options.onOwnershipIssue==="throw")blocking.push(...escalatedFindings(finding.findings,"ownership"));const evidence=deepFreeze({schemaVersion:EVIDENCE_MODEL_SCHEMA_VERSION,provenance:{generatedAt:generatedAt.toISOString(),toolVersion:readToolVersion(),commit:void 0},contract,dependency,ownership,lifecycle,finding,change});return {root,manifestOptions,manifestOutputPath,manifestComputed,docsOptions,docsPath,envExamplePath,docsComputed,docsContracts,usageOptions,usageReportPath,usageComputed,evidencePath,evidence,blocking,packageWarnings:[...packageWarnings,...tsconfigWarnings],linkWarnings:linkResult.warnings,generatedAt}}async function generateEnvArtifacts(options){const computed=await computeArtifacts(options);const{manifestOptions,manifestOutputPath,manifestComputed,docsOptions,docsPath,envExamplePath,docsComputed,docsContracts,usageOptions,usageReportPath,usageComputed,evidencePath,evidence,blocking,packageWarnings,linkWarnings,root,generatedAt}=computed;if(blocking.length>0)throw new EnvProjectGenerationError(blocking);let manifestResult;if(manifestOptions&&manifestComputed&&manifestOutputPath){await writeManifest(manifestOutputPath,manifestComputed.activeContracts,options.fs);manifestResult={outputPath:manifestOutputPath,contracts:manifestComputed.contractSummaries,warnings:manifestComputed.warnings,parseWarnings:[...packageWarnings,...linkWarnings]};}let docsResult;if(docsOptions&&docsPath){const{envExample}=await writeDocumentation(docsPath,envExamplePath,root,docsContracts,docsComputed.contractModelContracts,docsComputed.documentation,docsOptions.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS,generatedAt,options.fs,docsOptions.envExample?.onExisting);docsResult={docsPath,envExample,contracts:docsComputed.contractSummaries,catalog:docsComputed.catalog,parseWarnings:[...packageWarnings,...linkWarnings],documentation:docsComputed.documentation};}let usageResult;if(usageOptions){if(usageReportPath)await writeUsageReport(usageReportPath,usageComputed.result,options.fs);usageResult={reportPath:usageReportPath,...usageComputed.result};}if(evidencePath){await writeEvidenceSnapshot(evidencePath,evidence,options.fs);const fingerprint=await computeSourceFingerprint({fs:options.fs,root,include:options.include??defaultInclude(),exclude:options.exclude??defaultExclude(),packages:options.packages??[]});await writeEvidenceFingerprint(evidencePath,fingerprint,options.fs);}return {manifest:manifestResult,docs:docsResult,usage:usageResult,evidence}}async function readIfExists(filePath,fs){try{return await fs.readFile(filePath,"utf8")}catch{return void 0}}function normalizeEvidenceJsonForComparison(text){try{const parsed=JSON.parse(text);return JSON.stringify(normalizeEvidenceSnapshotForComparison(parsed),null,2)}catch{return text}}async function compareTextArtifact(artifact,filePath,expected,fs,normalize=s=>s){const actual=await readIfExists(filePath,fs);if(actual===void 0)return {artifact,path:filePath,status:"missing",detail:"not yet generated"};if(normalize(actual)===normalize(expected))return {artifact,path:filePath,status:"ok"};return {artifact,path:filePath,status:"stale",detail:"generated content differs from what's committed"}}async function checkEnvArtifacts(options){const c=await computeArtifacts(options);if(c.blocking.length>0)throw new EnvProjectGenerationError(c.blocking);const findings=[];if(c.manifestOptions&&c.manifestComputed&&c.manifestOutputPath){const expected=renderManifest(c.manifestComputed.activeContracts,c.manifestOutputPath);findings.push(await compareTextArtifact("manifest",c.manifestOutputPath,expected,options.fs));}if(c.docsOptions&&c.docsPath){const previousContent=await readIfExists(c.docsPath,options.fs);const expected=renderDocs(c.docsComputed.contractModelContracts,{expiringWithinDays:c.docsOptions.expiringWithinDays??DEFAULT_EXPIRING_WITHIN_DAYS,undocumentedContracts:c.docsComputed.documentation.undocumentedContracts.map(ref=>relativizeRef(c.root,ref)),undocumentedVariables:c.docsComputed.documentation.undocumentedVariables.map(ref=>relativizeRef(c.root,ref)),generatedAt:c.generatedAt,previousContent});findings.push(await compareTextArtifact("docs",c.docsPath,expected,options.fs,normalizeDocsForComparison));if(c.docsOptions.envExample&&c.envExamplePath){const existing=await readIfExists(c.envExamplePath,options.fs);if(existing===void 0){findings.push({artifact:"envExample",path:c.envExamplePath,status:"missing",detail:"not yet generated"});}else {const{staleVariables,variablesToComment,variablesToAdd}=computeReconciliation(c.docsContracts,existing);const driftCount=staleVariables.length+variablesToComment.length+variablesToAdd.length;findings.push({artifact:"envExample",path:c.envExamplePath,status:driftCount>0?"stale":"ok",detail:driftCount>0?`${staleVariables.length} stale, ${variablesToComment.length} to comment, ${variablesToAdd.length} to add`:void 0});}}}if(c.usageOptions&&c.usageReportPath){const expected=renderUsageReport(c.usageComputed.result);findings.push(await compareTextArtifact("usage",c.usageReportPath,expected,options.fs));}if(c.evidencePath){const expected=JSON.stringify(c.evidence,null,2);findings.push(await compareTextArtifact("evidence",c.evidencePath,expected,options.fs,normalizeEvidenceJsonForComparison));}return {ok:findings.every(f=>f.status==="ok"),findings}}function sarifLevel(severity){return severity==="info"?"note":severity}function sarifLocations(finding){const location=finding.location;const uri=location.model==="change"?location.path:location.file;if(uri===void 0)return void 0;const position=location.model==="change"?void 0:location.position;return [{physicalLocation:{artifactLocation:{uri},...position===void 0?{}:{region:{startLine:position.line,startColumn:position.column}}}}]}function buildSarifLog(findingModel){const ruleIds=[...new Set(findingModel.findings.map(f=>f.code))].sort();return {$schema:"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json",version:"2.1.0",runs:[{tool:{driver:{name:"env-cap",informationUri:"https://github.com/maverickcer/env-cap#readme",version:readToolVersion(),rules:ruleIds.map(id=>({id}))}},results:findingModel.findings.map(finding=>({ruleId:finding.code,level:sarifLevel(finding.severity),message:{text:finding.message},locations:sarifLocations(finding)}))}]}}
24
24
 
25
25
  exports.CHANGE_MODEL_SCHEMA_VERSION = CHANGE_MODEL_SCHEMA_VERSION;