@wrongstack/techstack 0.306.3 → 0.307.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/index.d.ts CHANGED
@@ -42,8 +42,10 @@ export { createAuditRunner, isNativeAuditAvailable, runNativeAudit, runNpmAudit,
42
42
  export type { AuditCommandResult, AuditCommandRunner, ConfiguredAuditRunner, NativeAdvisory, NativeAuditResult, } from './advisory/native-audit.js';
43
43
  export { classifyStatus, compareVersions, failedLookupStatus, privateOrUnresolvedStatus, } from './policy/status.js';
44
44
  export type { AdvisoryStatusData, RegistryStatusData } from './policy/status.js';
45
+ export { assessLicense, createLicenseFinding, normalizeLicenseId, type LicenseCategory, type LicenseRiskAssessment, } from './policy/license.js';
46
+ export { detectWorkspaceMisalignments, type VersionMisalignment, } from './policy/misalignment.js';
45
47
  export { TechStackEngine } from './service.js';
46
- export type { AnalyzeOptions, EnrichOptions } from './service.js';
48
+ export type { AnalyzeOptions, EnrichOptions, ReportFormat } from './service.js';
47
49
  export { clusterCandidates, createProviderLlm, createResearcher, createToolSearch, parseResearchJson, triageCandidates, DEFAULT_TRIAGE_LIMIT, } from './research/index.js';
48
50
  export type { CreateResearcherOptions, LlmAccessor, ResearchCluster, ResearchLlm, ResearchLlmRequest, ResearchOptions, ResearchSearch, ResearchSearchResult, SearchToolOptions, TechStackResearcher, TriageCandidate, TriageOptions, } from './research/index.js';
49
51
  export { TechStackStore } from './store/sqlite.js';
package/dist/index.js CHANGED
@@ -2274,7 +2274,11 @@ function toSpdx(snapshot) {
2274
2274
  SPDXID: `SPDXRef-Package-${index}`,
2275
2275
  versionInfo: dep.locked ?? dep.requested,
2276
2276
  downloadLocation: dep.purl ? `https://purl.io/${dep.purl}` : "NOASSERTION",
2277
- licenseConcluded: dep.license ?? "NOASSERTION"
2277
+ filesAnalyzed: false,
2278
+ licenseConcluded: dep.license ?? "NOASSERTION",
2279
+ licenseDeclared: dep.license ?? "NOASSERTION",
2280
+ supplier: "NOASSERTION",
2281
+ copyrightText: "NOASSERTION"
2278
2282
  }))
2279
2283
  };
2280
2284
  }
@@ -3585,6 +3589,229 @@ function failedLookupStatus(source, error) {
3585
3589
  };
3586
3590
  }
3587
3591
 
3592
+ // src/policy/license.ts
3593
+ var PERMISSIVE_LICENSES = /* @__PURE__ */ new Set([
3594
+ "mit",
3595
+ "apache-2.0",
3596
+ "bsd-2-clause",
3597
+ "bsd-3-clause",
3598
+ "isc",
3599
+ "unlicense",
3600
+ "cc0-1.0",
3601
+ "0bsd",
3602
+ "zlib",
3603
+ "wtfpl"
3604
+ ]);
3605
+ var WEAK_COPYLEFT_LICENSES = /* @__PURE__ */ new Set([
3606
+ "lgpl-2.0",
3607
+ "lgpl-2.1",
3608
+ "lgpl-3.0",
3609
+ "mpl-2.0",
3610
+ "cddl-1.0",
3611
+ "epl-1.0",
3612
+ "epl-2.0"
3613
+ ]);
3614
+ var STRONG_COPYLEFT_LICENSES = /* @__PURE__ */ new Set([
3615
+ "gpl-2.0",
3616
+ "gpl-2.0-only",
3617
+ "gpl-2.0-or-later",
3618
+ "gpl-3.0",
3619
+ "gpl-3.0-only",
3620
+ "gpl-3.0-or-later",
3621
+ "eupl-1.1",
3622
+ "eupl-1.2",
3623
+ "osl-3.0"
3624
+ ]);
3625
+ var NETWORK_COPYLEFT_LICENSES = /* @__PURE__ */ new Set([
3626
+ "agpl-3.0",
3627
+ "agpl-3.0-only",
3628
+ "agpl-3.0-or-later",
3629
+ "sspl-1.0"
3630
+ ]);
3631
+ var RESTRICTIVE_LICENSES = /* @__PURE__ */ new Set([
3632
+ "bsl-1.1",
3633
+ "busl-1.1",
3634
+ "cc-by-nc-4.0",
3635
+ "cc-by-nc-sa-4.0",
3636
+ "commons-clause"
3637
+ ]);
3638
+ function normalizeLicenseId(rawLicense) {
3639
+ if (!rawLicense) return "";
3640
+ return rawLicense.trim().toLowerCase().replace(/[()]/g, "").replace(/\s+or\s+/g, "/").replace(/\s+and\s+/g, "+");
3641
+ }
3642
+ function assessLicense(rawLicense) {
3643
+ if (!rawLicense || rawLicense.trim() === "") {
3644
+ return {
3645
+ license: rawLicense ?? "None",
3646
+ category: "unknown",
3647
+ severity: "low",
3648
+ isCopyleft: false,
3649
+ isCommercialSafe: false,
3650
+ rationale: "No license declared. Default copyright restrictions apply."
3651
+ };
3652
+ }
3653
+ const normalized = normalizeLicenseId(rawLicense);
3654
+ if (normalized === "unlicensed" || normalized === "proprietary") {
3655
+ return {
3656
+ license: rawLicense,
3657
+ category: "unlicensed",
3658
+ severity: "medium",
3659
+ isCopyleft: false,
3660
+ isCommercialSafe: false,
3661
+ rationale: "Unlicensed or proprietary dependency. May require explicit vendor permission."
3662
+ };
3663
+ }
3664
+ if (NETWORK_COPYLEFT_LICENSES.has(normalized)) {
3665
+ return {
3666
+ license: rawLicense,
3667
+ category: "network_copyleft",
3668
+ severity: "high",
3669
+ isCopyleft: true,
3670
+ isCommercialSafe: false,
3671
+ rationale: `${rawLicense} carries strict network copyleft terms. Network access may trigger source disclosure requirements.`
3672
+ };
3673
+ }
3674
+ if (STRONG_COPYLEFT_LICENSES.has(normalized)) {
3675
+ return {
3676
+ license: rawLicense,
3677
+ category: "strong_copyleft",
3678
+ severity: "high",
3679
+ isCopyleft: true,
3680
+ isCommercialSafe: false,
3681
+ rationale: `${rawLicense} is viral copyleft. Distributing software including this package may require open sourcing the entire codebase under GPL.`
3682
+ };
3683
+ }
3684
+ if (RESTRICTIVE_LICENSES.has(normalized)) {
3685
+ return {
3686
+ license: rawLicense,
3687
+ category: "restrictive",
3688
+ severity: "high",
3689
+ isCopyleft: false,
3690
+ isCommercialSafe: false,
3691
+ rationale: `${rawLicense} contains non-commercial or source-available restrictions. Check commercial eligibility.`
3692
+ };
3693
+ }
3694
+ if (WEAK_COPYLEFT_LICENSES.has(normalized)) {
3695
+ return {
3696
+ license: rawLicense,
3697
+ category: "weak_copyleft",
3698
+ severity: "info",
3699
+ isCopyleft: true,
3700
+ isCommercialSafe: true,
3701
+ rationale: `${rawLicense} is weak copyleft (file/module level). Safe for proprietary projects when dynamically linked.`
3702
+ };
3703
+ }
3704
+ if (PERMISSIVE_LICENSES.has(normalized)) {
3705
+ return {
3706
+ license: rawLicense,
3707
+ category: "permissive",
3708
+ severity: "info",
3709
+ isCopyleft: false,
3710
+ isCommercialSafe: true,
3711
+ rationale: `${rawLicense} is a standard permissive license. Safe for commercial and closed-source use.`
3712
+ };
3713
+ }
3714
+ if (normalized.includes("gpl") || normalized.includes("agpl")) {
3715
+ return {
3716
+ license: rawLicense,
3717
+ category: "strong_copyleft",
3718
+ severity: "medium",
3719
+ isCopyleft: true,
3720
+ isCommercialSafe: false,
3721
+ rationale: `License expression "${rawLicense}" appears to reference GPL/AGPL terms. Review compliance obligations.`
3722
+ };
3723
+ }
3724
+ return {
3725
+ license: rawLicense,
3726
+ category: "unknown",
3727
+ severity: "info",
3728
+ isCopyleft: false,
3729
+ isCommercialSafe: true,
3730
+ rationale: `Custom or non-standard license string "${rawLicense}". Verify terms if distributing binary releases.`
3731
+ };
3732
+ }
3733
+ function createLicenseFinding(dependencyId, dependencyName, rawLicense) {
3734
+ const assessment = assessLicense(rawLicense);
3735
+ if (assessment.category === "permissive" || assessment.category === "weak_copyleft") {
3736
+ return null;
3737
+ }
3738
+ return {
3739
+ id: `finding-${dependencyId}-license`,
3740
+ dependencyId,
3741
+ type: "license",
3742
+ severity: assessment.severity,
3743
+ action: assessment.isCopyleft ? "investigate" : "none",
3744
+ confidence: 1,
3745
+ rationale: `[${assessment.license}] ${dependencyName} \u2014 ${assessment.rationale}`,
3746
+ evidence: [
3747
+ {
3748
+ kind: "registry",
3749
+ source: "license-classifier",
3750
+ retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
3751
+ detail: `Category: ${assessment.category}, Commercial-Safe: ${assessment.isCommercialSafe}`
3752
+ }
3753
+ ]
3754
+ };
3755
+ }
3756
+
3757
+ // src/policy/misalignment.ts
3758
+ function detectWorkspaceMisalignments(dependencies, workspaces) {
3759
+ if (workspaces.length <= 1 || dependencies.length === 0) {
3760
+ return [];
3761
+ }
3762
+ const workspaceRootById = /* @__PURE__ */ new Map();
3763
+ for (const ws of workspaces) {
3764
+ workspaceRootById.set(ws.id, ws.relativeRoot || ".");
3765
+ }
3766
+ const byPackage = /* @__PURE__ */ new Map();
3767
+ for (const dep of dependencies) {
3768
+ if (dep.sourceType === "path" || dep.sourceType === "git") continue;
3769
+ const key = `${dep.ecosystem}:${dep.name}`;
3770
+ const list = byPackage.get(key) ?? [];
3771
+ list.push(dep);
3772
+ byPackage.set(key, list);
3773
+ }
3774
+ const findings = [];
3775
+ for (const [_key, deps] of byPackage) {
3776
+ if (deps.length <= 1) continue;
3777
+ const versionByWs = /* @__PURE__ */ new Map();
3778
+ for (const dep of deps) {
3779
+ const version = dep.locked ?? dep.requested ?? "unknown";
3780
+ versionByWs.set(dep.workspaceId, version);
3781
+ }
3782
+ const uniqueVersions = new Set(versionByWs.values());
3783
+ if (uniqueVersions.size <= 1) continue;
3784
+ const summaryParts = [];
3785
+ for (const [wsId, ver] of versionByWs) {
3786
+ const root = workspaceRootById.get(wsId) ?? wsId;
3787
+ summaryParts.push(`${root} (${ver})`);
3788
+ }
3789
+ const versionSummary = summaryParts.join(", ");
3790
+ for (const dep of deps) {
3791
+ const currentVer = dep.locked ?? dep.requested ?? "unknown";
3792
+ const root = workspaceRootById.get(dep.workspaceId) ?? dep.workspaceId;
3793
+ findings.push({
3794
+ id: `finding-${dep.id}-misalignment`,
3795
+ dependencyId: dep.id,
3796
+ type: "investigate",
3797
+ severity: "info",
3798
+ action: "investigate",
3799
+ confidence: 1,
3800
+ rationale: `Version mismatch across workspaces: ${root} uses ${currentVer} while other packages differ [${versionSummary}]`,
3801
+ evidence: [
3802
+ {
3803
+ kind: "manifest",
3804
+ source: "monorepo-alignment",
3805
+ retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
3806
+ detail: `Workspace distribution: ${versionSummary}`
3807
+ }
3808
+ ]
3809
+ });
3810
+ }
3811
+ }
3812
+ return findings;
3813
+ }
3814
+
3588
3815
  // src/service/techstack-engine.ts
3589
3816
  import { randomUUID as randomUUID2 } from "node:crypto";
3590
3817
 
@@ -3668,20 +3895,26 @@ async function runEnrichPhase(snapshot, options = {}) {
3668
3895
  if (dependency.name !== name) continue;
3669
3896
  const status = classifyStatus(dependency, registryStatus, advisoryStatus);
3670
3897
  const evidence = [...dependency.evidence, ...registryStatus?.evidence ?? [], ...advisoryStatus?.evidence ?? []];
3898
+ const license = registryEntry?.license ?? dependency.license;
3671
3899
  enriched.set(dependency.id, {
3672
3900
  ...dependency,
3673
3901
  latestStable: registryEntry?.latestStable ?? dependency.latestStable,
3674
- license: registryEntry?.license ?? dependency.license,
3902
+ license,
3675
3903
  deprecated: registryEntry?.deprecated ?? dependency.deprecated,
3676
3904
  yanked: registryEntry?.yanked ?? dependency.yanked,
3677
3905
  status,
3678
3906
  evidence
3679
3907
  });
3680
3908
  if (status !== "current" && status !== "local_path" && status !== "git_dependency") findings.push(createFindingForStatus(dependency.id, status));
3909
+ const licenseFinding = createLicenseFinding(dependency.id, dependency.name, license);
3910
+ if (licenseFinding) findings.push(licenseFinding);
3681
3911
  }
3682
3912
  }
3683
3913
  }
3684
- return { ...snapshot, dependencies: snapshot.dependencies.map((dependency) => enriched.get(dependency.id) ?? dependency), findings };
3914
+ const enrichedDeps = snapshot.dependencies.map((dependency) => enriched.get(dependency.id) ?? dependency);
3915
+ const misalignmentFindings = detectWorkspaceMisalignments(enrichedDeps, snapshot.workspaces);
3916
+ findings.push(...misalignmentFindings);
3917
+ return { ...snapshot, dependencies: enrichedDeps, findings };
3685
3918
  }
3686
3919
 
3687
3920
  // src/service/inventory-phase.ts
@@ -3742,6 +3975,8 @@ async function runInventoryPhase(store, projectId, targetRoot, options = {}) {
3742
3975
  // src/service/report-generator.ts
3743
3976
  function generateReport(snapshot, format = "md") {
3744
3977
  if (format === "json") return JSON.stringify(snapshot, null, 2);
3978
+ if (format === "spdx") return JSON.stringify(toSpdx(snapshot), null, 2);
3979
+ if (format === "cyclonedx") return JSON.stringify(toCycloneDX(snapshot), null, 2);
3745
3980
  const lines = [
3746
3981
  "# TechStack Report",
3747
3982
  "",
@@ -4353,6 +4588,15 @@ CREATE TABLE IF NOT EXISTS outbox (
4353
4588
  );
4354
4589
 
4355
4590
  CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);
4591
+
4592
+ CREATE TABLE IF NOT EXISTS research_cache (
4593
+ cache_key TEXT PRIMARY KEY,
4594
+ findings_json TEXT NOT NULL,
4595
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4596
+ expires_at TEXT NOT NULL
4597
+ );
4598
+
4599
+ CREATE INDEX IF NOT EXISTS idx_research_cache_expires_at ON research_cache(expires_at);
4356
4600
  `;
4357
4601
  function applySchema(db) {
4358
4602
  for (const statement of DDL.split(";")) {
@@ -4600,6 +4844,54 @@ var TechStackStore = class {
4600
4844
  const rows = stmt.all(status);
4601
4845
  return rows.map((r) => this.rowToOutbox(r));
4602
4846
  }
4847
+ // ── Research Cache Operations ───────────────────────────────────────────
4848
+ /** Get cached research findings by cache key (returns null if missing or expired). */
4849
+ getCachedResearch(cacheKey) {
4850
+ const stmt = this.stmt(`
4851
+ SELECT findings_json, expires_at FROM research_cache
4852
+ WHERE cache_key = ?
4853
+ `);
4854
+ const row = stmt.get(cacheKey);
4855
+ if (!row) return null;
4856
+ if (new Date(row.expires_at).getTime() <= Date.now()) {
4857
+ this.deleteCachedResearch(cacheKey);
4858
+ return null;
4859
+ }
4860
+ try {
4861
+ return JSON.parse(row.findings_json);
4862
+ } catch {
4863
+ return null;
4864
+ }
4865
+ }
4866
+ /** Store research findings in cache with a TTL (defaults to 7 days). */
4867
+ setCachedResearch(cacheKey, findings, ttlMs = 7 * 24 * 60 * 60 * 1e3) {
4868
+ const expiresAt = new Date(Date.now() + ttlMs).toISOString();
4869
+ const findingsJson = JSON.stringify(findings);
4870
+ const stmt = this.stmt(`
4871
+ INSERT INTO research_cache (cache_key, findings_json, created_at, expires_at)
4872
+ VALUES (?, ?, datetime('now'), ?)
4873
+ ON CONFLICT(cache_key) DO UPDATE SET
4874
+ findings_json = excluded.findings_json,
4875
+ created_at = datetime('now'),
4876
+ expires_at = excluded.expires_at
4877
+ `);
4878
+ stmt.run(cacheKey, findingsJson, expiresAt);
4879
+ }
4880
+ /** Delete a specific cached research entry. */
4881
+ deleteCachedResearch(cacheKey) {
4882
+ const stmt = this.stmt(`
4883
+ DELETE FROM research_cache WHERE cache_key = ?
4884
+ `);
4885
+ stmt.run(cacheKey);
4886
+ }
4887
+ /** Prune all expired research cache entries. */
4888
+ pruneExpiredResearchCache() {
4889
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4890
+ const stmt = this.stmt(`
4891
+ DELETE FROM research_cache WHERE expires_at <= ?
4892
+ `);
4893
+ stmt.run(nowIso);
4894
+ }
4603
4895
  // ── Row mapping helpers ─────────────────────────────────────────────────
4604
4896
  rowToJob(row) {
4605
4897
  let progress;
@@ -4718,6 +5010,7 @@ export {
4718
5010
  TrendStore,
4719
5011
  applyPlan,
4720
5012
  applySchema,
5013
+ assessLicense,
4721
5014
  attemptDelivery,
4722
5015
  buildPurl,
4723
5016
  classifyStatus,
@@ -4728,10 +5021,12 @@ export {
4728
5021
  coverageForEcosystem,
4729
5022
  cppAdapter,
4730
5023
  createAuditRunner,
5024
+ createLicenseFinding,
4731
5025
  createProviderLlm,
4732
5026
  createResearcher,
4733
5027
  createToolSearch,
4734
5028
  dartAdapter,
5029
+ detectWorkspaceMisalignments,
4735
5030
  diffSnapshots,
4736
5031
  discoverWorkspaces,
4737
5032
  dotNetAdapter,
@@ -4748,6 +5043,7 @@ export {
4748
5043
  lookupRegistryBatch,
4749
5044
  mapDetectedWorkspace,
4750
5045
  mavenAdapter,
5046
+ normalizeLicenseId,
4751
5047
  npmAdapter,
4752
5048
  parsePurl,
4753
5049
  parsePurlEcosystem,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * TechStack — License Risk & Compliance Classification.
3
+ *
4
+ * Categorizes open source software licenses by risk profile (Permissive,
5
+ * Weak Copyleft, Strong Viral Copyleft, Restrictive/Commercial, Unknown)
6
+ * and generates deterministic compliance findings.
7
+ *
8
+ * @see docs/specs/techstack-sdd.md §4.1, §7
9
+ */
10
+ import type { Finding } from '../types.js';
11
+ export type LicenseCategory = 'permissive' | 'weak_copyleft' | 'strong_copyleft' | 'network_copyleft' | 'restrictive' | 'unlicensed' | 'unknown';
12
+ export interface LicenseRiskAssessment {
13
+ readonly license: string;
14
+ readonly category: LicenseCategory;
15
+ readonly severity: 'info' | 'low' | 'medium' | 'high' | 'critical';
16
+ readonly isCopyleft: boolean;
17
+ readonly isCommercialSafe: boolean;
18
+ readonly rationale: string;
19
+ }
20
+ /** Normalize raw license identifier string for SPDX lookup. */
21
+ export declare function normalizeLicenseId(rawLicense: string | undefined): string;
22
+ /** Assess the compliance and legal risk of a license. */
23
+ export declare function assessLicense(rawLicense: string | undefined): LicenseRiskAssessment;
24
+ /** Generate a License Compliance Finding if the license represents notable risk. */
25
+ export declare function createLicenseFinding(dependencyId: string, dependencyName: string, rawLicense: string | undefined): Finding | null;
26
+ //# sourceMappingURL=license.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * TechStack — Cross-Workspace Version Drift & Misalignment Detector.
3
+ *
4
+ * In monorepo projects, different workspaces (packages/apps) may declare
5
+ * or resolve different versions of the same dependency, leading to bundle
6
+ * bloat, runtime inconsistencies, and duplicate lockfile resolutions.
7
+ *
8
+ * @see docs/specs/techstack-sdd.md §4.1, §7
9
+ */
10
+ import type { DependencyObservation, Finding, Workspace } from '../types.js';
11
+ export interface VersionMisalignment {
12
+ readonly ecosystem: string;
13
+ readonly name: string;
14
+ readonly distinctVersions: readonly string[];
15
+ readonly occurrences: ReadonlyArray<{
16
+ readonly dependencyId: string;
17
+ readonly workspaceId: string;
18
+ readonly workspaceRoot: string;
19
+ readonly version: string;
20
+ }>;
21
+ }
22
+ /**
23
+ * Detects third-party dependency version mismatches across multiple workspaces.
24
+ */
25
+ export declare function detectWorkspaceMisalignments(dependencies: readonly DependencyObservation[], workspaces: readonly Workspace[]): readonly Finding[];
26
+ //# sourceMappingURL=misalignment.d.ts.map
package/dist/sbom.d.ts CHANGED
@@ -21,7 +21,11 @@ export interface SpdxDocument {
21
21
  SPDXID: string;
22
22
  versionInfo?: string | undefined;
23
23
  downloadLocation?: string | undefined;
24
+ filesAnalyzed: boolean;
24
25
  licenseConcluded?: string | undefined;
26
+ licenseDeclared?: string | undefined;
27
+ supplier: string;
28
+ copyrightText: string;
25
29
  }>;
26
30
  }
27
31
  export declare function toSpdx(snapshot: Snapshot): SpdxDocument;
@@ -1,3 +1,4 @@
1
1
  import type { Snapshot } from '../types.js';
2
- export declare function generateReport(snapshot: Snapshot, format?: 'md' | 'json'): string;
2
+ export type ReportFormat = 'md' | 'json' | 'spdx' | 'cyclonedx';
3
+ export declare function generateReport(snapshot: Snapshot, format?: ReportFormat): string;
3
4
  //# sourceMappingURL=report-generator.d.ts.map
@@ -3,8 +3,10 @@ import type { TechStackResearcher } from '../research/types.js';
3
3
  import type { TechStackStore } from '../store/sqlite.js';
4
4
  import type { Snapshot, TechStackJob } from '../types.js';
5
5
  import { type EnrichOptions } from './enrich-phase.js';
6
+ import { type ReportFormat } from './report-generator.js';
6
7
  import { type ResearchPhaseOptions } from './research-phase.js';
7
8
  export type { EnrichOptions } from './enrich-phase.js';
9
+ export type { ReportFormat } from './report-generator.js';
8
10
  export interface AnalyzeOptions {
9
11
  readonly targetRoot: string;
10
12
  readonly sessionId?: string | undefined;
@@ -24,7 +26,7 @@ export declare class TechStackEngine {
24
26
  inventory(projectId: string, targetRoot: string, _jobId?: string, onProgress?: (phase: string, completed: number, total: number) => void, options?: InventoryOptions): Promise<Snapshot>;
25
27
  enrich(snapshot: Snapshot, options?: EnrichOptions): Promise<Snapshot>;
26
28
  research(snapshot: Snapshot, options?: ResearchPhaseOptions): Promise<Snapshot>;
27
- generateReport(snapshot: Snapshot, format?: 'md' | 'json'): string;
29
+ generateReport(snapshot: Snapshot, format?: ReportFormat): string;
28
30
  analyze(projectId: string, options: AnalyzeOptions): Promise<{
29
31
  snapshot: Snapshot;
30
32
  job: TechStackJob;
package/dist/service.d.ts CHANGED
@@ -5,5 +5,5 @@
5
5
  * `service/`; existing imports from `service.js` remain stable.
6
6
  */
7
7
  export { TechStackEngine } from './service/techstack-engine.js';
8
- export type { AnalyzeOptions, EnrichOptions } from './service/techstack-engine.js';
8
+ export type { AnalyzeOptions, EnrichOptions, ReportFormat } from './service/techstack-engine.js';
9
9
  //# sourceMappingURL=service.d.ts.map
@@ -12,7 +12,7 @@
12
12
  * @see docs/specs/techstack-sdd.md §3.2, §4.1
13
13
  */
14
14
  export declare const SCHEMA_VERSION = 1;
15
- export declare const DDL = "\nCREATE TABLE IF NOT EXISTS techstack_schema_version (\n version INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n raw_json TEXT NOT NULL,\n adapter_version TEXT NOT NULL DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_snapshots_project_id ON snapshots(project_id);\nCREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at DESC);\n\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('inventory', 'analyze')),\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','discovering','inventorying','enriching','researching','synthesizing','completed','failed','cancelled')),\n fingerprint TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n session_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n error TEXT,\n progress_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_jobs_project_id ON jobs(project_id);\nCREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);\n\nCREATE TABLE IF NOT EXISTS outbox (\n delivery_id TEXT PRIMARY KEY,\n report_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK(status IN ('pending', 'claimed', 'delivered', 'failed')),\n attempts INTEGER NOT NULL DEFAULT 0,\n claimed_at TEXT,\n delivered_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);\n";
15
+ export declare const DDL = "\nCREATE TABLE IF NOT EXISTS techstack_schema_version (\n version INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS snapshots (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n fingerprint TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n raw_json TEXT NOT NULL,\n adapter_version TEXT NOT NULL DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_snapshots_project_id ON snapshots(project_id);\nCREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at DESC);\n\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n target_root TEXT NOT NULL,\n kind TEXT NOT NULL CHECK(kind IN ('inventory', 'analyze')),\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','discovering','inventorying','enriching','researching','synthesizing','completed','failed','cancelled')),\n fingerprint TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n session_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT,\n error TEXT,\n progress_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_jobs_project_id ON jobs(project_id);\nCREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);\n\nCREATE TABLE IF NOT EXISTS outbox (\n delivery_id TEXT PRIMARY KEY,\n report_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending'\n CHECK(status IN ('pending', 'claimed', 'delivered', 'failed')),\n attempts INTEGER NOT NULL DEFAULT 0,\n claimed_at TEXT,\n delivered_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);\n\nCREATE TABLE IF NOT EXISTS research_cache (\n cache_key TEXT PRIMARY KEY,\n findings_json TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n expires_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_research_cache_expires_at ON research_cache(expires_at);\n";
16
16
  /**
17
17
  * Run the schema DDL and check/migrate version.
18
18
  */
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @see docs/specs/techstack-sdd.md §3.2, §4.1
10
10
  */
11
- import type { DeliveryOutbox, DeliveryStatus, Snapshot, TechStackJob, TechStackJobStatus, TechStackJobProgress } from '../types.js';
11
+ import type { DeliveryOutbox, DeliveryStatus, Finding, Snapshot, TechStackJob, TechStackJobStatus, TechStackJobProgress } from '../types.js';
12
12
  export interface StoreOptions {
13
13
  /** Project slug used for the store directory path. */
14
14
  readonly projectSlug: string;
@@ -55,6 +55,14 @@ export declare class TechStackStore {
55
55
  failOutbox(deliveryId: string): void;
56
56
  /** List outbox entries by status. */
57
57
  listOutboxByStatus(status: DeliveryStatus): DeliveryOutbox[];
58
+ /** Get cached research findings by cache key (returns null if missing or expired). */
59
+ getCachedResearch(cacheKey: string): readonly Finding[] | null;
60
+ /** Store research findings in cache with a TTL (defaults to 7 days). */
61
+ setCachedResearch(cacheKey: string, findings: readonly Finding[], ttlMs?: number): void;
62
+ /** Delete a specific cached research entry. */
63
+ deleteCachedResearch(cacheKey: string): void;
64
+ /** Prune all expired research cache entries. */
65
+ pruneExpiredResearchCache(): void;
58
66
  private rowToJob;
59
67
  private rowToOutbox;
60
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/techstack",
3
- "version": "0.306.3",
3
+ "version": "0.307.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack TechStack — cross-language dependency intelligence for the active target project: discover, inventory, enrich, analyze, and report.",
6
6
  "repository": {
@@ -26,11 +26,11 @@
26
26
  "!dist/**/*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@wrongstack/tools": "0.306.3",
30
- "@wrongstack/core": "0.306.3"
29
+ "@wrongstack/core": "0.307.0",
30
+ "@wrongstack/tools": "0.307.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^26.1.2",
33
+ "@types/node": "^26.2.0",
34
34
  "typescript": "^7.0.2"
35
35
  },
36
36
  "publishConfig": {
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.