@harness-engineering/core 0.19.0 → 0.20.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/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/index.d.mts +1348 -85
- package/dist/index.d.ts +1348 -85
- package/dist/index.js +1635 -642
- package/dist/index.mjs +1571 -604
- package/dist/{matchers-Dj1t5vpg.d.mts → matchers-D20x48U9.d.mts} +46 -46
- package/dist/{matchers-Dj1t5vpg.d.ts → matchers-D20x48U9.d.ts} +46 -46
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -84,15 +84,15 @@ function validateConfig(data, schema) {
|
|
|
84
84
|
let message = "Configuration validation failed";
|
|
85
85
|
const suggestions = [];
|
|
86
86
|
if (firstError) {
|
|
87
|
-
const
|
|
88
|
-
const pathDisplay =
|
|
87
|
+
const path28 = firstError.path.join(".");
|
|
88
|
+
const pathDisplay = path28 ? ` at "${path28}"` : "";
|
|
89
89
|
if (firstError.code === "invalid_type") {
|
|
90
90
|
const received = firstError.received;
|
|
91
91
|
const expected = firstError.expected;
|
|
92
92
|
if (received === "undefined") {
|
|
93
93
|
code = "MISSING_FIELD";
|
|
94
94
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
95
|
-
suggestions.push(`Field "${
|
|
95
|
+
suggestions.push(`Field "${path28}" is required and must be of type "${expected}"`);
|
|
96
96
|
} else {
|
|
97
97
|
code = "INVALID_TYPE";
|
|
98
98
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -308,27 +308,27 @@ function extractSections(content) {
|
|
|
308
308
|
}
|
|
309
309
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
310
310
|
}
|
|
311
|
-
function isExternalLink(
|
|
312
|
-
return
|
|
311
|
+
function isExternalLink(path28) {
|
|
312
|
+
return path28.startsWith("http://") || path28.startsWith("https://") || path28.startsWith("#") || path28.startsWith("mailto:");
|
|
313
313
|
}
|
|
314
314
|
function resolveLinkPath(linkPath, baseDir) {
|
|
315
315
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
316
316
|
}
|
|
317
|
-
async function validateAgentsMap(
|
|
318
|
-
const contentResult = await readFileContent(
|
|
317
|
+
async function validateAgentsMap(path28 = "./AGENTS.md") {
|
|
318
|
+
const contentResult = await readFileContent(path28);
|
|
319
319
|
if (!contentResult.ok) {
|
|
320
320
|
return Err(
|
|
321
321
|
createError(
|
|
322
322
|
"PARSE_ERROR",
|
|
323
323
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
324
|
-
{ path:
|
|
324
|
+
{ path: path28 },
|
|
325
325
|
["Ensure the file exists", "Check file permissions"]
|
|
326
326
|
)
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
const content = contentResult.value;
|
|
330
330
|
const sections = extractSections(content);
|
|
331
|
-
const baseDir = dirname(
|
|
331
|
+
const baseDir = dirname(path28);
|
|
332
332
|
const sectionTitles = sections.map((s) => s.title);
|
|
333
333
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
334
334
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -469,8 +469,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
469
469
|
|
|
470
470
|
// src/context/knowledge-map.ts
|
|
471
471
|
import { join as join2, basename as basename2 } from "path";
|
|
472
|
-
function suggestFix(
|
|
473
|
-
const targetName = basename2(
|
|
472
|
+
function suggestFix(path28, existingFiles) {
|
|
473
|
+
const targetName = basename2(path28).toLowerCase();
|
|
474
474
|
const similar = existingFiles.find((file) => {
|
|
475
475
|
const fileName = basename2(file).toLowerCase();
|
|
476
476
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -478,7 +478,7 @@ function suggestFix(path26, existingFiles) {
|
|
|
478
478
|
if (similar) {
|
|
479
479
|
return `Did you mean "${similar}"?`;
|
|
480
480
|
}
|
|
481
|
-
return `Create the file "${
|
|
481
|
+
return `Create the file "${path28}" or remove the link`;
|
|
482
482
|
}
|
|
483
483
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
484
484
|
const agentsPath = join2(rootDir, "AGENTS.md");
|
|
@@ -830,8 +830,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
830
830
|
return Ok(result.data);
|
|
831
831
|
}
|
|
832
832
|
const suggestions = result.error.issues.map((issue) => {
|
|
833
|
-
const
|
|
834
|
-
return
|
|
833
|
+
const path28 = issue.path.join(".");
|
|
834
|
+
return path28 ? `${path28}: ${issue.message}` : issue.message;
|
|
835
835
|
});
|
|
836
836
|
return Err(
|
|
837
837
|
createError(
|
|
@@ -1463,11 +1463,11 @@ function processExportListSpecifiers(exportDecl, exports) {
|
|
|
1463
1463
|
var TypeScriptParser = class {
|
|
1464
1464
|
name = "typescript";
|
|
1465
1465
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1466
|
-
async parseFile(
|
|
1467
|
-
const contentResult = await readFileContent(
|
|
1466
|
+
async parseFile(path28) {
|
|
1467
|
+
const contentResult = await readFileContent(path28);
|
|
1468
1468
|
if (!contentResult.ok) {
|
|
1469
1469
|
return Err(
|
|
1470
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1470
|
+
createParseError("NOT_FOUND", `File not found: ${path28}`, { path: path28 }, [
|
|
1471
1471
|
"Check that the file exists",
|
|
1472
1472
|
"Verify the path is correct"
|
|
1473
1473
|
])
|
|
@@ -1477,7 +1477,7 @@ var TypeScriptParser = class {
|
|
|
1477
1477
|
const ast = parse(contentResult.value, {
|
|
1478
1478
|
loc: true,
|
|
1479
1479
|
range: true,
|
|
1480
|
-
jsx:
|
|
1480
|
+
jsx: path28.endsWith(".tsx"),
|
|
1481
1481
|
errorOnUnknownASTType: false
|
|
1482
1482
|
});
|
|
1483
1483
|
return Ok({
|
|
@@ -1488,7 +1488,7 @@ var TypeScriptParser = class {
|
|
|
1488
1488
|
} catch (e) {
|
|
1489
1489
|
const error = e;
|
|
1490
1490
|
return Err(
|
|
1491
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1491
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path28}: ${error.message}`, { path: path28 }, [
|
|
1492
1492
|
"Check for syntax errors in the file",
|
|
1493
1493
|
"Ensure valid TypeScript syntax"
|
|
1494
1494
|
])
|
|
@@ -1673,22 +1673,22 @@ function extractInlineRefs(content) {
|
|
|
1673
1673
|
}
|
|
1674
1674
|
return refs;
|
|
1675
1675
|
}
|
|
1676
|
-
async function parseDocumentationFile(
|
|
1677
|
-
const contentResult = await readFileContent(
|
|
1676
|
+
async function parseDocumentationFile(path28) {
|
|
1677
|
+
const contentResult = await readFileContent(path28);
|
|
1678
1678
|
if (!contentResult.ok) {
|
|
1679
1679
|
return Err(
|
|
1680
1680
|
createEntropyError(
|
|
1681
1681
|
"PARSE_ERROR",
|
|
1682
|
-
`Failed to read documentation file: ${
|
|
1683
|
-
{ file:
|
|
1682
|
+
`Failed to read documentation file: ${path28}`,
|
|
1683
|
+
{ file: path28 },
|
|
1684
1684
|
["Check that the file exists"]
|
|
1685
1685
|
)
|
|
1686
1686
|
);
|
|
1687
1687
|
}
|
|
1688
1688
|
const content = contentResult.value;
|
|
1689
|
-
const type =
|
|
1689
|
+
const type = path28.endsWith(".md") ? "markdown" : "text";
|
|
1690
1690
|
return Ok({
|
|
1691
|
-
path:
|
|
1691
|
+
path: path28,
|
|
1692
1692
|
type,
|
|
1693
1693
|
content,
|
|
1694
1694
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -4681,10 +4681,10 @@ function resolveThresholds(scope, config) {
|
|
|
4681
4681
|
}
|
|
4682
4682
|
const merged = { ...projectThresholds };
|
|
4683
4683
|
for (const [category, moduleValue] of Object.entries(moduleOverrides)) {
|
|
4684
|
-
const
|
|
4685
|
-
if (
|
|
4684
|
+
const projectValue2 = projectThresholds[category];
|
|
4685
|
+
if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
|
|
4686
4686
|
merged[category] = {
|
|
4687
|
-
...
|
|
4687
|
+
...projectValue2,
|
|
4688
4688
|
...moduleValue
|
|
4689
4689
|
};
|
|
4690
4690
|
} else {
|
|
@@ -4694,77 +4694,1173 @@ function resolveThresholds(scope, config) {
|
|
|
4694
4694
|
return merged;
|
|
4695
4695
|
}
|
|
4696
4696
|
|
|
4697
|
-
// src/
|
|
4697
|
+
// src/architecture/timeline-types.ts
|
|
4698
4698
|
import { z as z3 } from "zod";
|
|
4699
|
-
var
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4699
|
+
var CategorySnapshotSchema = z3.object({
|
|
4700
|
+
/** Aggregate metric value (e.g., violation count, avg complexity) */
|
|
4701
|
+
value: z3.number(),
|
|
4702
|
+
/** Count of violations in this category */
|
|
4703
|
+
violationCount: z3.number()
|
|
4704
|
+
});
|
|
4705
|
+
var TimelineSnapshotSchema = z3.object({
|
|
4706
|
+
/** ISO 8601 timestamp of capture */
|
|
4707
|
+
capturedAt: z3.string().datetime(),
|
|
4708
|
+
/** Git commit hash at capture time */
|
|
4709
|
+
commitHash: z3.string(),
|
|
4710
|
+
/** Composite stability score (0-100, higher is healthier) */
|
|
4711
|
+
stabilityScore: z3.number().min(0).max(100),
|
|
4712
|
+
/** Per-category metric aggregates */
|
|
4713
|
+
metrics: z3.record(ArchMetricCategorySchema, CategorySnapshotSchema)
|
|
4714
|
+
});
|
|
4715
|
+
var TimelineFileSchema = z3.object({
|
|
4716
|
+
version: z3.literal(1),
|
|
4717
|
+
snapshots: z3.array(TimelineSnapshotSchema)
|
|
4718
|
+
});
|
|
4719
|
+
var TrendLineSchema = z3.object({
|
|
4720
|
+
/** Current value */
|
|
4721
|
+
current: z3.number(),
|
|
4722
|
+
/** Previous value (from comparison snapshot) */
|
|
4723
|
+
previous: z3.number(),
|
|
4724
|
+
/** Absolute delta (current - previous) */
|
|
4725
|
+
delta: z3.number(),
|
|
4726
|
+
/** Direction indicator */
|
|
4727
|
+
direction: z3.enum(["improving", "stable", "declining"])
|
|
4728
|
+
});
|
|
4729
|
+
var TrendResultSchema = z3.object({
|
|
4730
|
+
/** Overall stability trend */
|
|
4731
|
+
stability: TrendLineSchema,
|
|
4732
|
+
/** Per-category trends */
|
|
4733
|
+
categories: z3.record(ArchMetricCategorySchema, TrendLineSchema),
|
|
4734
|
+
/** Number of snapshots analyzed */
|
|
4735
|
+
snapshotCount: z3.number(),
|
|
4736
|
+
/** Time range covered */
|
|
4737
|
+
from: z3.string(),
|
|
4738
|
+
to: z3.string()
|
|
4739
|
+
});
|
|
4740
|
+
var DEFAULT_STABILITY_THRESHOLDS = {
|
|
4741
|
+
"circular-deps": 5,
|
|
4742
|
+
"layer-violations": 10,
|
|
4743
|
+
complexity: 100,
|
|
4744
|
+
coupling: 2,
|
|
4745
|
+
"forbidden-imports": 5,
|
|
4746
|
+
"module-size": 10,
|
|
4747
|
+
"dependency-depth": 10
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
// src/architecture/timeline-manager.ts
|
|
4751
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
4752
|
+
import { randomBytes } from "crypto";
|
|
4753
|
+
import { join as join8, dirname as dirname8 } from "path";
|
|
4754
|
+
var ALL_CATEGORIES = ArchMetricCategorySchema.options;
|
|
4755
|
+
var TimelineManager = class {
|
|
4756
|
+
timelinePath;
|
|
4757
|
+
constructor(rootDir) {
|
|
4758
|
+
this.timelinePath = join8(rootDir, ".harness", "arch", "timeline.json");
|
|
4759
|
+
}
|
|
4760
|
+
/**
|
|
4761
|
+
* Load timeline from disk.
|
|
4762
|
+
* Returns empty TimelineFile if file does not exist or is invalid.
|
|
4763
|
+
*/
|
|
4764
|
+
load() {
|
|
4765
|
+
if (!existsSync3(this.timelinePath)) {
|
|
4766
|
+
return { version: 1, snapshots: [] };
|
|
4767
|
+
}
|
|
4768
|
+
try {
|
|
4769
|
+
const raw = readFileSync3(this.timelinePath, "utf-8");
|
|
4770
|
+
const data = JSON.parse(raw);
|
|
4771
|
+
const parsed = TimelineFileSchema.safeParse(data);
|
|
4772
|
+
if (!parsed.success) {
|
|
4773
|
+
console.error(
|
|
4774
|
+
`Timeline validation failed for ${this.timelinePath}:`,
|
|
4775
|
+
parsed.error.format()
|
|
4776
|
+
);
|
|
4777
|
+
return { version: 1, snapshots: [] };
|
|
4778
|
+
}
|
|
4779
|
+
return parsed.data;
|
|
4780
|
+
} catch (error) {
|
|
4781
|
+
console.error(`Error loading timeline from ${this.timelinePath}:`, error);
|
|
4782
|
+
return { version: 1, snapshots: [] };
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
/**
|
|
4786
|
+
* Save timeline to disk using atomic write (temp file + rename).
|
|
4787
|
+
* Creates parent directories if they do not exist.
|
|
4788
|
+
*/
|
|
4789
|
+
save(timeline) {
|
|
4790
|
+
const dir = dirname8(this.timelinePath);
|
|
4791
|
+
if (!existsSync3(dir)) {
|
|
4792
|
+
mkdirSync3(dir, { recursive: true });
|
|
4793
|
+
}
|
|
4794
|
+
const tmp = this.timelinePath + "." + randomBytes(4).toString("hex") + ".tmp";
|
|
4795
|
+
writeFileSync3(tmp, JSON.stringify(timeline, null, 2));
|
|
4796
|
+
renameSync(tmp, this.timelinePath);
|
|
4797
|
+
}
|
|
4798
|
+
/**
|
|
4799
|
+
* Capture a new snapshot from current metric results.
|
|
4800
|
+
* Aggregates MetricResult[] by category, computes stability score,
|
|
4801
|
+
* appends to timeline (or replaces if same commitHash), and saves.
|
|
4802
|
+
*/
|
|
4803
|
+
capture(results, commitHash) {
|
|
4804
|
+
const metrics = this.aggregateByCategory(results);
|
|
4805
|
+
const stabilityScore = this.computeStabilityScore(metrics);
|
|
4806
|
+
const snapshot = {
|
|
4807
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4808
|
+
commitHash,
|
|
4809
|
+
stabilityScore,
|
|
4810
|
+
metrics
|
|
4811
|
+
};
|
|
4812
|
+
const timeline = this.load();
|
|
4813
|
+
const lastIndex = timeline.snapshots.length - 1;
|
|
4814
|
+
if (lastIndex >= 0 && timeline.snapshots[lastIndex].commitHash === commitHash) {
|
|
4815
|
+
timeline.snapshots[lastIndex] = snapshot;
|
|
4816
|
+
} else {
|
|
4817
|
+
timeline.snapshots.push(snapshot);
|
|
4818
|
+
}
|
|
4819
|
+
this.save(timeline);
|
|
4820
|
+
return snapshot;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* Compute trends between snapshots over a window.
|
|
4824
|
+
* @param options.last - Number of recent snapshots to analyze (default: 10)
|
|
4825
|
+
* @param options.since - ISO date string to filter snapshots from
|
|
4826
|
+
*/
|
|
4827
|
+
trends(options) {
|
|
4828
|
+
const timeline = this.load();
|
|
4829
|
+
let snapshots = timeline.snapshots;
|
|
4830
|
+
if (options?.since) {
|
|
4831
|
+
const sinceDate = new Date(options.since);
|
|
4832
|
+
snapshots = snapshots.filter((s) => new Date(s.capturedAt) >= sinceDate);
|
|
4833
|
+
}
|
|
4834
|
+
if (options?.last && snapshots.length > options.last) {
|
|
4835
|
+
snapshots = snapshots.slice(-options.last);
|
|
4836
|
+
}
|
|
4837
|
+
if (snapshots.length === 0) {
|
|
4838
|
+
return this.emptyTrendResult();
|
|
4839
|
+
}
|
|
4840
|
+
if (snapshots.length === 1) {
|
|
4841
|
+
const only = snapshots[0];
|
|
4842
|
+
const m = only.metrics;
|
|
4843
|
+
return {
|
|
4844
|
+
stability: this.buildTrendLine(only.stabilityScore, only.stabilityScore, true),
|
|
4845
|
+
categories: this.buildCategoryTrends(m, m),
|
|
4846
|
+
snapshotCount: 1,
|
|
4847
|
+
from: only.capturedAt,
|
|
4848
|
+
to: only.capturedAt
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
const first = snapshots[0];
|
|
4852
|
+
const last = snapshots[snapshots.length - 1];
|
|
4853
|
+
return {
|
|
4854
|
+
stability: this.buildTrendLine(last.stabilityScore, first.stabilityScore, true),
|
|
4855
|
+
categories: this.buildCategoryTrends(
|
|
4856
|
+
last.metrics,
|
|
4857
|
+
first.metrics
|
|
4858
|
+
),
|
|
4859
|
+
snapshotCount: snapshots.length,
|
|
4860
|
+
from: first.capturedAt,
|
|
4861
|
+
to: last.capturedAt
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Compute composite stability score from category metrics.
|
|
4866
|
+
* Equal weight across all categories. Score is 0-100 (higher = healthier).
|
|
4867
|
+
* health = max(0, 1 - (value / threshold)) per category.
|
|
4868
|
+
*/
|
|
4869
|
+
computeStabilityScore(metrics, thresholds = DEFAULT_STABILITY_THRESHOLDS) {
|
|
4870
|
+
const healthScores = [];
|
|
4871
|
+
for (const category of ALL_CATEGORIES) {
|
|
4872
|
+
const snapshot = metrics[category];
|
|
4873
|
+
if (!snapshot) {
|
|
4874
|
+
healthScores.push(1);
|
|
4875
|
+
continue;
|
|
4876
|
+
}
|
|
4877
|
+
const threshold = thresholds[category] ?? 10;
|
|
4878
|
+
const health = Math.max(0, 1 - snapshot.value / threshold);
|
|
4879
|
+
healthScores.push(health);
|
|
4880
|
+
}
|
|
4881
|
+
const mean = healthScores.reduce((sum, h) => sum + h, 0) / healthScores.length;
|
|
4882
|
+
return Math.round(mean * 100);
|
|
4883
|
+
}
|
|
4884
|
+
// --- Private helpers ---
|
|
4885
|
+
aggregateByCategory(results) {
|
|
4886
|
+
const metrics = {};
|
|
4887
|
+
for (const result of results) {
|
|
4888
|
+
const existing = metrics[result.category];
|
|
4889
|
+
if (existing) {
|
|
4890
|
+
existing.value += result.value;
|
|
4891
|
+
existing.violationCount += result.violations.length;
|
|
4892
|
+
} else {
|
|
4893
|
+
metrics[result.category] = {
|
|
4894
|
+
value: result.value,
|
|
4895
|
+
violationCount: result.violations.length
|
|
4896
|
+
};
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
for (const category of ALL_CATEGORIES) {
|
|
4900
|
+
if (!metrics[category]) {
|
|
4901
|
+
metrics[category] = { value: 0, violationCount: 0 };
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
return metrics;
|
|
4905
|
+
}
|
|
4906
|
+
buildTrendLine(current, previous, isStabilityScore) {
|
|
4907
|
+
const delta = current - previous;
|
|
4908
|
+
let direction;
|
|
4909
|
+
if (Math.abs(delta) < 2) {
|
|
4910
|
+
direction = "stable";
|
|
4911
|
+
} else if (isStabilityScore) {
|
|
4912
|
+
direction = delta > 0 ? "improving" : "declining";
|
|
4913
|
+
} else {
|
|
4914
|
+
direction = delta < 0 ? "improving" : "declining";
|
|
4915
|
+
}
|
|
4916
|
+
return { current, previous, delta, direction };
|
|
4917
|
+
}
|
|
4918
|
+
buildCategoryTrends(currentMetrics, previousMetrics) {
|
|
4919
|
+
const trends = {};
|
|
4920
|
+
for (const category of ALL_CATEGORIES) {
|
|
4921
|
+
const current = currentMetrics[category]?.value ?? 0;
|
|
4922
|
+
const previous = previousMetrics[category]?.value ?? 0;
|
|
4923
|
+
trends[category] = this.buildTrendLine(current, previous, false);
|
|
4924
|
+
}
|
|
4925
|
+
return trends;
|
|
4926
|
+
}
|
|
4927
|
+
emptyTrendResult() {
|
|
4928
|
+
const zeroLine = { current: 0, previous: 0, delta: 0, direction: "stable" };
|
|
4929
|
+
const categories = {};
|
|
4930
|
+
for (const category of ALL_CATEGORIES) {
|
|
4931
|
+
categories[category] = { ...zeroLine };
|
|
4932
|
+
}
|
|
4933
|
+
return {
|
|
4934
|
+
stability: { ...zeroLine },
|
|
4935
|
+
categories,
|
|
4936
|
+
snapshotCount: 0,
|
|
4937
|
+
from: "",
|
|
4938
|
+
to: ""
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
};
|
|
4942
|
+
|
|
4943
|
+
// src/architecture/prediction-types.ts
|
|
4944
|
+
import { z as z4 } from "zod";
|
|
4945
|
+
var ConfidenceTierSchema = z4.enum(["high", "medium", "low"]);
|
|
4946
|
+
var RegressionResultSchema = z4.object({
|
|
4947
|
+
slope: z4.number(),
|
|
4948
|
+
intercept: z4.number(),
|
|
4949
|
+
rSquared: z4.number().min(0).max(1),
|
|
4950
|
+
dataPoints: z4.number().int().min(0)
|
|
4951
|
+
});
|
|
4952
|
+
var DirectionSchema = z4.enum(["improving", "stable", "declining"]);
|
|
4953
|
+
var CategoryForecastSchema = z4.object({
|
|
4954
|
+
category: ArchMetricCategorySchema,
|
|
4955
|
+
current: z4.number(),
|
|
4956
|
+
threshold: z4.number(),
|
|
4957
|
+
projectedValue4w: z4.number(),
|
|
4958
|
+
projectedValue8w: z4.number(),
|
|
4959
|
+
projectedValue12w: z4.number(),
|
|
4960
|
+
thresholdCrossingWeeks: z4.number().nullable(),
|
|
4961
|
+
confidence: ConfidenceTierSchema,
|
|
4962
|
+
regression: RegressionResultSchema,
|
|
4963
|
+
direction: DirectionSchema
|
|
4964
|
+
});
|
|
4965
|
+
var SpecImpactSignalsSchema = z4.object({
|
|
4966
|
+
newFileCount: z4.number().int().min(0),
|
|
4967
|
+
affectedLayers: z4.array(z4.string()),
|
|
4968
|
+
newDependencies: z4.number().int().min(0),
|
|
4969
|
+
phaseCount: z4.number().int().min(0)
|
|
4970
|
+
});
|
|
4971
|
+
var SpecImpactEstimateSchema = z4.object({
|
|
4972
|
+
specPath: z4.string(),
|
|
4973
|
+
featureName: z4.string(),
|
|
4974
|
+
signals: SpecImpactSignalsSchema,
|
|
4975
|
+
deltas: z4.record(ArchMetricCategorySchema, z4.number()).optional()
|
|
4976
|
+
});
|
|
4977
|
+
var ContributingFeatureSchema = z4.object({
|
|
4978
|
+
name: z4.string(),
|
|
4979
|
+
specPath: z4.string(),
|
|
4980
|
+
delta: z4.number()
|
|
4981
|
+
});
|
|
4982
|
+
var AdjustedForecastSchema = z4.object({
|
|
4983
|
+
baseline: CategoryForecastSchema,
|
|
4984
|
+
adjusted: CategoryForecastSchema,
|
|
4985
|
+
contributingFeatures: z4.array(ContributingFeatureSchema)
|
|
4986
|
+
});
|
|
4987
|
+
var PredictionWarningSchema = z4.object({
|
|
4988
|
+
severity: z4.enum(["critical", "warning", "info"]),
|
|
4989
|
+
category: ArchMetricCategorySchema,
|
|
4990
|
+
message: z4.string(),
|
|
4991
|
+
weeksUntil: z4.number(),
|
|
4992
|
+
confidence: ConfidenceTierSchema,
|
|
4993
|
+
contributingFeatures: z4.array(z4.string())
|
|
4994
|
+
});
|
|
4995
|
+
var StabilityForecastSchema = z4.object({
|
|
4996
|
+
current: z4.number(),
|
|
4997
|
+
projected4w: z4.number(),
|
|
4998
|
+
projected8w: z4.number(),
|
|
4999
|
+
projected12w: z4.number(),
|
|
5000
|
+
confidence: ConfidenceTierSchema,
|
|
5001
|
+
direction: DirectionSchema
|
|
5002
|
+
});
|
|
5003
|
+
var PredictionResultSchema = z4.object({
|
|
5004
|
+
generatedAt: z4.string(),
|
|
5005
|
+
snapshotsUsed: z4.number().int().min(0),
|
|
5006
|
+
timelineRange: z4.object({
|
|
5007
|
+
from: z4.string(),
|
|
5008
|
+
to: z4.string()
|
|
5009
|
+
}),
|
|
5010
|
+
stabilityForecast: StabilityForecastSchema,
|
|
5011
|
+
categories: z4.record(ArchMetricCategorySchema, AdjustedForecastSchema),
|
|
5012
|
+
warnings: z4.array(PredictionWarningSchema)
|
|
5013
|
+
});
|
|
5014
|
+
var PredictionOptionsSchema = z4.object({
|
|
5015
|
+
horizon: z4.number().int().min(1).default(12),
|
|
5016
|
+
includeRoadmap: z4.boolean().default(true),
|
|
5017
|
+
categories: z4.array(ArchMetricCategorySchema).optional(),
|
|
5018
|
+
thresholds: z4.record(ArchMetricCategorySchema, z4.number()).optional()
|
|
4704
5019
|
});
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
5020
|
+
|
|
5021
|
+
// src/architecture/regression.ts
|
|
5022
|
+
function computeWeightedSums(points) {
|
|
5023
|
+
let sumW = 0, sumWt = 0, sumWv = 0, sumWtt = 0, sumWtv = 0;
|
|
5024
|
+
for (const p of points) {
|
|
5025
|
+
const w = p.weight;
|
|
5026
|
+
sumW += w;
|
|
5027
|
+
sumWt += w * p.t;
|
|
5028
|
+
sumWv += w * p.value;
|
|
5029
|
+
sumWtt += w * p.t * p.t;
|
|
5030
|
+
sumWtv += w * p.t * p.value;
|
|
5031
|
+
}
|
|
5032
|
+
return { sumW, sumWt, sumWv, sumWtt, sumWtv };
|
|
5033
|
+
}
|
|
5034
|
+
function computeRSquared(points, slope, intercept, meanV) {
|
|
5035
|
+
let ssRes = 0, ssTot = 0;
|
|
5036
|
+
for (const p of points) {
|
|
5037
|
+
const predicted = slope * p.t + intercept;
|
|
5038
|
+
ssRes += p.weight * (p.value - predicted) ** 2;
|
|
5039
|
+
ssTot += p.weight * (p.value - meanV) ** 2;
|
|
5040
|
+
}
|
|
5041
|
+
return ssTot < 1e-12 ? 1 : Math.max(0, 1 - ssRes / ssTot);
|
|
5042
|
+
}
|
|
5043
|
+
function weightedLinearRegression(points) {
|
|
5044
|
+
if (points.length < 2) {
|
|
5045
|
+
throw new Error(`Regression requires at least 2 data points, got ${points.length}`);
|
|
5046
|
+
}
|
|
5047
|
+
const n = points.length;
|
|
5048
|
+
const { sumW, sumWt, sumWv, sumWtt, sumWtv } = computeWeightedSums(points);
|
|
5049
|
+
const meanT = sumWt / sumW;
|
|
5050
|
+
const meanV = sumWv / sumW;
|
|
5051
|
+
const denominator = sumWtt - sumWt * sumWt / sumW;
|
|
5052
|
+
if (Math.abs(denominator) < 1e-12) {
|
|
5053
|
+
return { slope: 0, intercept: meanV, rSquared: 0, dataPoints: n };
|
|
5054
|
+
}
|
|
5055
|
+
const slope = (sumWtv - sumWt * sumWv / sumW) / denominator;
|
|
5056
|
+
const intercept = meanV - slope * meanT;
|
|
5057
|
+
const rSquared = computeRSquared(points, slope, intercept, meanV);
|
|
5058
|
+
return { slope, intercept, rSquared, dataPoints: n };
|
|
5059
|
+
}
|
|
5060
|
+
function applyRecencyWeights(values, decay = 0.85) {
|
|
5061
|
+
const n = values.length;
|
|
5062
|
+
return values.map((v, i) => ({
|
|
5063
|
+
t: v.t,
|
|
5064
|
+
value: v.value,
|
|
5065
|
+
weight: Math.pow(decay, n - 1 - i)
|
|
5066
|
+
}));
|
|
5067
|
+
}
|
|
5068
|
+
function projectValue(fit, t) {
|
|
5069
|
+
return fit.slope * t + fit.intercept;
|
|
5070
|
+
}
|
|
5071
|
+
function weeksUntilThreshold(fit, currentT, threshold) {
|
|
5072
|
+
if (fit.slope <= 0) {
|
|
5073
|
+
return null;
|
|
5074
|
+
}
|
|
5075
|
+
const currentProjected = projectValue(fit, currentT);
|
|
5076
|
+
if (currentProjected >= threshold) {
|
|
5077
|
+
return null;
|
|
5078
|
+
}
|
|
5079
|
+
const weeks = (threshold - currentProjected) / fit.slope;
|
|
5080
|
+
return Math.ceil(weeks);
|
|
5081
|
+
}
|
|
5082
|
+
function classifyConfidence(rSquared, dataPoints) {
|
|
5083
|
+
if (rSquared >= 0.7 && dataPoints >= 5) return "high";
|
|
5084
|
+
if (rSquared >= 0.4 && dataPoints >= 3) return "medium";
|
|
5085
|
+
return "low";
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
// src/architecture/prediction-engine.ts
|
|
5089
|
+
import * as fs5 from "fs";
|
|
5090
|
+
import * as path2 from "path";
|
|
5091
|
+
|
|
5092
|
+
// src/roadmap/parse.ts
|
|
5093
|
+
import { Ok as Ok2, Err as Err2 } from "@harness-engineering/types";
|
|
5094
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
5095
|
+
"backlog",
|
|
5096
|
+
"planned",
|
|
5097
|
+
"in-progress",
|
|
5098
|
+
"done",
|
|
5099
|
+
"blocked"
|
|
5100
|
+
]);
|
|
5101
|
+
var EM_DASH = "\u2014";
|
|
5102
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
5103
|
+
function parseRoadmap(markdown) {
|
|
5104
|
+
const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
5105
|
+
if (!fmMatch) {
|
|
5106
|
+
return Err2(new Error("Missing or malformed YAML frontmatter"));
|
|
5107
|
+
}
|
|
5108
|
+
const fmResult = parseFrontmatter(fmMatch[1]);
|
|
5109
|
+
if (!fmResult.ok) return fmResult;
|
|
5110
|
+
const body = markdown.slice(fmMatch[0].length);
|
|
5111
|
+
const milestonesResult = parseMilestones(body);
|
|
5112
|
+
if (!milestonesResult.ok) return milestonesResult;
|
|
5113
|
+
const historyResult = parseAssignmentHistory(body);
|
|
5114
|
+
if (!historyResult.ok) return historyResult;
|
|
5115
|
+
return Ok2({
|
|
5116
|
+
frontmatter: fmResult.value,
|
|
5117
|
+
milestones: milestonesResult.value,
|
|
5118
|
+
assignmentHistory: historyResult.value
|
|
5119
|
+
});
|
|
5120
|
+
}
|
|
5121
|
+
function parseFrontmatter(raw) {
|
|
5122
|
+
const lines = raw.split("\n");
|
|
5123
|
+
const map = /* @__PURE__ */ new Map();
|
|
5124
|
+
for (const line of lines) {
|
|
5125
|
+
const idx = line.indexOf(":");
|
|
5126
|
+
if (idx === -1) continue;
|
|
5127
|
+
const key = line.slice(0, idx).trim();
|
|
5128
|
+
const val = line.slice(idx + 1).trim();
|
|
5129
|
+
map.set(key, val);
|
|
5130
|
+
}
|
|
5131
|
+
const project = map.get("project");
|
|
5132
|
+
const versionStr = map.get("version");
|
|
5133
|
+
const lastSynced = map.get("last_synced");
|
|
5134
|
+
const lastManualEdit = map.get("last_manual_edit");
|
|
5135
|
+
const created = map.get("created");
|
|
5136
|
+
const updated = map.get("updated");
|
|
5137
|
+
if (!project || !versionStr || !lastSynced || !lastManualEdit) {
|
|
5138
|
+
return Err2(
|
|
5139
|
+
new Error(
|
|
5140
|
+
"Frontmatter missing required fields: project, version, last_synced, last_manual_edit"
|
|
5141
|
+
)
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
const version = parseInt(versionStr, 10);
|
|
5145
|
+
if (isNaN(version)) {
|
|
5146
|
+
return Err2(new Error("Frontmatter version must be a number"));
|
|
5147
|
+
}
|
|
5148
|
+
const fm = { project, version, lastSynced, lastManualEdit };
|
|
5149
|
+
if (created) fm.created = created;
|
|
5150
|
+
if (updated) fm.updated = updated;
|
|
5151
|
+
return Ok2(fm);
|
|
5152
|
+
}
|
|
5153
|
+
function parseMilestones(body) {
|
|
5154
|
+
const milestones = [];
|
|
5155
|
+
const h2Pattern = /^## (.+)$/gm;
|
|
5156
|
+
const h2Matches = [];
|
|
5157
|
+
let match;
|
|
5158
|
+
let bodyEnd = body.length;
|
|
5159
|
+
while ((match = h2Pattern.exec(body)) !== null) {
|
|
5160
|
+
if (match[1] === "Assignment History") {
|
|
5161
|
+
bodyEnd = match.index;
|
|
5162
|
+
break;
|
|
5163
|
+
}
|
|
5164
|
+
h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
5165
|
+
}
|
|
5166
|
+
for (let i = 0; i < h2Matches.length; i++) {
|
|
5167
|
+
const h2 = h2Matches[i];
|
|
5168
|
+
const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
|
|
5169
|
+
const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
|
|
5170
|
+
const isBacklog = h2.heading === "Backlog";
|
|
5171
|
+
const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
|
|
5172
|
+
const featuresResult = parseFeatures(sectionBody);
|
|
5173
|
+
if (!featuresResult.ok) return featuresResult;
|
|
5174
|
+
milestones.push({
|
|
5175
|
+
name: milestoneName,
|
|
5176
|
+
isBacklog,
|
|
5177
|
+
features: featuresResult.value
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
return Ok2(milestones);
|
|
5181
|
+
}
|
|
5182
|
+
function parseFeatures(sectionBody) {
|
|
5183
|
+
const features = [];
|
|
5184
|
+
const h3Pattern = /^### (?:Feature: )?(.+)$/gm;
|
|
5185
|
+
const h3Matches = [];
|
|
5186
|
+
let match;
|
|
5187
|
+
while ((match = h3Pattern.exec(sectionBody)) !== null) {
|
|
5188
|
+
h3Matches.push({ name: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
5189
|
+
}
|
|
5190
|
+
for (let i = 0; i < h3Matches.length; i++) {
|
|
5191
|
+
const h3 = h3Matches[i];
|
|
5192
|
+
const nextStart = i + 1 < h3Matches.length ? h3Matches[i + 1].startIndex : sectionBody.length;
|
|
5193
|
+
const featureBody = sectionBody.slice(h3.startIndex + h3.fullMatch.length, nextStart);
|
|
5194
|
+
const featureResult = parseFeatureFields(h3.name, featureBody);
|
|
5195
|
+
if (!featureResult.ok) return featureResult;
|
|
5196
|
+
features.push(featureResult.value);
|
|
5197
|
+
}
|
|
5198
|
+
return Ok2(features);
|
|
5199
|
+
}
|
|
5200
|
+
function extractFieldMap(body) {
|
|
5201
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
5202
|
+
const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
|
|
5203
|
+
let match;
|
|
5204
|
+
while ((match = fieldPattern.exec(body)) !== null) {
|
|
5205
|
+
fieldMap.set(match[1], match[2]);
|
|
5206
|
+
}
|
|
5207
|
+
return fieldMap;
|
|
5208
|
+
}
|
|
5209
|
+
function parseListField(fieldMap, ...keys) {
|
|
5210
|
+
let raw = EM_DASH;
|
|
5211
|
+
for (const key of keys) {
|
|
5212
|
+
const val = fieldMap.get(key);
|
|
5213
|
+
if (val !== void 0) {
|
|
5214
|
+
raw = val;
|
|
5215
|
+
break;
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
if (raw === EM_DASH || raw === "none") return [];
|
|
5219
|
+
return raw.split(",").map((s) => s.trim());
|
|
5220
|
+
}
|
|
5221
|
+
function parseFeatureFields(name, body) {
|
|
5222
|
+
const fieldMap = extractFieldMap(body);
|
|
5223
|
+
const statusRaw = fieldMap.get("Status");
|
|
5224
|
+
if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
|
|
5225
|
+
return Err2(
|
|
5226
|
+
new Error(
|
|
5227
|
+
`Feature "${name}" has invalid status: "${statusRaw ?? "(missing)"}". Valid statuses: ${[...VALID_STATUSES].join(", ")}`
|
|
5228
|
+
)
|
|
5229
|
+
);
|
|
5230
|
+
}
|
|
5231
|
+
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
5232
|
+
const plans = parseListField(fieldMap, "Plans", "Plan");
|
|
5233
|
+
const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
|
|
5234
|
+
const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
|
|
5235
|
+
const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
|
|
5236
|
+
const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
|
|
5237
|
+
if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
|
|
5238
|
+
return Err2(
|
|
5239
|
+
new Error(
|
|
5240
|
+
`Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
|
|
5241
|
+
)
|
|
5242
|
+
);
|
|
5243
|
+
}
|
|
5244
|
+
return Ok2({
|
|
5245
|
+
name,
|
|
5246
|
+
status: statusRaw,
|
|
5247
|
+
spec: specRaw === EM_DASH ? null : specRaw,
|
|
5248
|
+
plans,
|
|
5249
|
+
blockedBy,
|
|
5250
|
+
summary: fieldMap.get("Summary") ?? "",
|
|
5251
|
+
assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
|
|
5252
|
+
priority: priorityRaw === EM_DASH ? null : priorityRaw,
|
|
5253
|
+
externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
function parseAssignmentHistory(body) {
|
|
5257
|
+
const historyMatch = body.match(/^## Assignment History\s*\n/m);
|
|
5258
|
+
if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
|
|
5259
|
+
const historyStart = historyMatch.index + historyMatch[0].length;
|
|
5260
|
+
const rawHistoryBody = body.slice(historyStart);
|
|
5261
|
+
const nextH2 = rawHistoryBody.search(/^## /m);
|
|
5262
|
+
const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
|
|
5263
|
+
const records = [];
|
|
5264
|
+
const lines = historyBody.split("\n");
|
|
5265
|
+
let pastHeader = false;
|
|
5266
|
+
for (const line of lines) {
|
|
5267
|
+
const trimmed = line.trim();
|
|
5268
|
+
if (!trimmed.startsWith("|")) continue;
|
|
5269
|
+
if (!pastHeader) {
|
|
5270
|
+
if (trimmed.match(/^\|[-\s|]+\|$/)) {
|
|
5271
|
+
pastHeader = true;
|
|
5272
|
+
}
|
|
5273
|
+
continue;
|
|
5274
|
+
}
|
|
5275
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
5276
|
+
if (cells.length < 4) continue;
|
|
5277
|
+
const action = cells[2];
|
|
5278
|
+
if (!["assigned", "completed", "unassigned"].includes(action)) continue;
|
|
5279
|
+
records.push({
|
|
5280
|
+
feature: cells[0],
|
|
5281
|
+
assignee: cells[1],
|
|
5282
|
+
action,
|
|
5283
|
+
date: cells[3]
|
|
5284
|
+
});
|
|
5285
|
+
}
|
|
5286
|
+
return Ok2(records);
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
// src/architecture/prediction-engine.ts
|
|
5290
|
+
var ALL_CATEGORIES2 = ArchMetricCategorySchema.options;
|
|
5291
|
+
var DIRECTION_THRESHOLD = 1e-3;
|
|
5292
|
+
var PredictionEngine = class {
|
|
5293
|
+
constructor(rootDir, timelineManager, estimator) {
|
|
5294
|
+
this.rootDir = rootDir;
|
|
5295
|
+
this.timelineManager = timelineManager;
|
|
5296
|
+
this.estimator = estimator;
|
|
5297
|
+
}
|
|
5298
|
+
rootDir;
|
|
5299
|
+
timelineManager;
|
|
5300
|
+
estimator;
|
|
5301
|
+
/**
|
|
5302
|
+
* Produce a PredictionResult with per-category forecasts and warnings.
|
|
5303
|
+
* Throws if fewer than 3 snapshots are available.
|
|
5304
|
+
*/
|
|
5305
|
+
predict(options) {
|
|
5306
|
+
const opts = this.resolveOptions(options);
|
|
5307
|
+
const timeline = this.timelineManager.load();
|
|
5308
|
+
const snapshots = timeline.snapshots;
|
|
5309
|
+
if (snapshots.length < 3) {
|
|
5310
|
+
throw new Error(
|
|
5311
|
+
`PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
|
|
5312
|
+
);
|
|
5313
|
+
}
|
|
5314
|
+
const thresholds = this.resolveThresholds(opts);
|
|
5315
|
+
const categoriesToProcess = opts.categories ?? [...ALL_CATEGORIES2];
|
|
5316
|
+
const firstDate = new Date(snapshots[0].capturedAt).getTime();
|
|
5317
|
+
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
5318
|
+
const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
|
|
5319
|
+
const baselines = {};
|
|
5320
|
+
for (const category of ALL_CATEGORIES2) {
|
|
5321
|
+
const threshold = thresholds[category];
|
|
5322
|
+
const shouldProcess = categoriesToProcess.includes(category);
|
|
5323
|
+
if (!shouldProcess) {
|
|
5324
|
+
baselines[category] = this.zeroForecast(category, threshold);
|
|
5325
|
+
continue;
|
|
5326
|
+
}
|
|
5327
|
+
const timeSeries = this.extractTimeSeries(snapshots, category, firstDate);
|
|
5328
|
+
baselines[category] = this.forecastCategory(
|
|
5329
|
+
category,
|
|
5330
|
+
timeSeries,
|
|
5331
|
+
currentT,
|
|
5332
|
+
threshold,
|
|
5333
|
+
opts.horizon
|
|
5334
|
+
);
|
|
5335
|
+
}
|
|
5336
|
+
const specImpacts = this.computeSpecImpacts(opts);
|
|
5337
|
+
const categories = {};
|
|
5338
|
+
for (const category of ALL_CATEGORIES2) {
|
|
5339
|
+
const baseline = baselines[category];
|
|
5340
|
+
const threshold = thresholds[category];
|
|
5341
|
+
if (!specImpacts || specImpacts.length === 0) {
|
|
5342
|
+
categories[category] = {
|
|
5343
|
+
baseline,
|
|
5344
|
+
adjusted: baseline,
|
|
5345
|
+
contributingFeatures: []
|
|
5346
|
+
};
|
|
5347
|
+
continue;
|
|
5348
|
+
}
|
|
5349
|
+
let totalDelta = 0;
|
|
5350
|
+
const contributing = [];
|
|
5351
|
+
for (const impact of specImpacts) {
|
|
5352
|
+
const delta = impact.deltas?.[category] ?? 0;
|
|
5353
|
+
if (delta !== 0) {
|
|
5354
|
+
totalDelta += delta;
|
|
5355
|
+
contributing.push({
|
|
5356
|
+
name: impact.featureName,
|
|
5357
|
+
specPath: impact.specPath,
|
|
5358
|
+
delta
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
if (totalDelta === 0) {
|
|
5363
|
+
categories[category] = {
|
|
5364
|
+
baseline,
|
|
5365
|
+
adjusted: baseline,
|
|
5366
|
+
contributingFeatures: []
|
|
5367
|
+
};
|
|
5368
|
+
continue;
|
|
5369
|
+
}
|
|
5370
|
+
const adjusted = {
|
|
5371
|
+
...baseline,
|
|
5372
|
+
projectedValue4w: baseline.projectedValue4w + totalDelta,
|
|
5373
|
+
projectedValue8w: baseline.projectedValue8w + totalDelta,
|
|
5374
|
+
projectedValue12w: baseline.projectedValue12w + totalDelta
|
|
5375
|
+
};
|
|
5376
|
+
const adjustedFit = {
|
|
5377
|
+
slope: baseline.regression.slope,
|
|
5378
|
+
intercept: baseline.regression.intercept + totalDelta,
|
|
5379
|
+
rSquared: baseline.regression.rSquared,
|
|
5380
|
+
dataPoints: baseline.regression.dataPoints
|
|
5381
|
+
};
|
|
5382
|
+
adjusted.thresholdCrossingWeeks = weeksUntilThreshold(adjustedFit, currentT, threshold);
|
|
5383
|
+
adjusted.regression = {
|
|
5384
|
+
slope: adjustedFit.slope,
|
|
5385
|
+
intercept: adjustedFit.intercept,
|
|
5386
|
+
rSquared: adjustedFit.rSquared,
|
|
5387
|
+
dataPoints: adjustedFit.dataPoints
|
|
5388
|
+
};
|
|
5389
|
+
categories[category] = {
|
|
5390
|
+
baseline,
|
|
5391
|
+
adjusted,
|
|
5392
|
+
contributingFeatures: contributing
|
|
5393
|
+
};
|
|
5394
|
+
}
|
|
5395
|
+
const warnings = this.generateWarnings(
|
|
5396
|
+
categories,
|
|
5397
|
+
opts.horizon
|
|
5398
|
+
);
|
|
5399
|
+
const stabilityForecast = this.computeStabilityForecast(
|
|
5400
|
+
categories,
|
|
5401
|
+
thresholds,
|
|
5402
|
+
snapshots
|
|
5403
|
+
);
|
|
5404
|
+
return {
|
|
5405
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5406
|
+
snapshotsUsed: snapshots.length,
|
|
5407
|
+
timelineRange: {
|
|
5408
|
+
from: snapshots[0].capturedAt,
|
|
5409
|
+
to: lastSnapshot.capturedAt
|
|
5410
|
+
},
|
|
5411
|
+
stabilityForecast,
|
|
5412
|
+
categories,
|
|
5413
|
+
warnings
|
|
5414
|
+
};
|
|
5415
|
+
}
|
|
5416
|
+
// --- Private helpers ---
|
|
5417
|
+
resolveOptions(options) {
|
|
5418
|
+
return {
|
|
5419
|
+
horizon: options?.horizon ?? 12,
|
|
5420
|
+
includeRoadmap: options?.includeRoadmap ?? true,
|
|
5421
|
+
categories: options?.categories,
|
|
5422
|
+
thresholds: options?.thresholds
|
|
5423
|
+
};
|
|
5424
|
+
}
|
|
5425
|
+
resolveThresholds(opts) {
|
|
5426
|
+
const base = { ...DEFAULT_STABILITY_THRESHOLDS };
|
|
5427
|
+
if (opts.thresholds) {
|
|
5428
|
+
for (const [key, value] of Object.entries(opts.thresholds)) {
|
|
5429
|
+
if (value !== void 0) {
|
|
5430
|
+
base[key] = value;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
return base;
|
|
5435
|
+
}
|
|
5436
|
+
/**
|
|
5437
|
+
* Extract time series for a single category from snapshots.
|
|
5438
|
+
* Returns array of { t (weeks from first), value } sorted oldest first.
|
|
5439
|
+
*/
|
|
5440
|
+
extractTimeSeries(snapshots, category, firstDateMs) {
|
|
5441
|
+
return snapshots.map((s) => {
|
|
5442
|
+
const t = (new Date(s.capturedAt).getTime() - firstDateMs) / (7 * 24 * 60 * 60 * 1e3);
|
|
5443
|
+
const metrics = s.metrics;
|
|
5444
|
+
const value = metrics[category]?.value ?? 0;
|
|
5445
|
+
return { t, value };
|
|
5446
|
+
});
|
|
5447
|
+
}
|
|
5448
|
+
/**
|
|
5449
|
+
* Produce a CategoryForecast for a single category using regression.
|
|
5450
|
+
*/
|
|
5451
|
+
forecastCategory(category, timeSeries, currentT, threshold, horizon = 12) {
|
|
5452
|
+
const weighted = applyRecencyWeights(timeSeries, 0.85);
|
|
5453
|
+
const fit = weightedLinearRegression(weighted);
|
|
5454
|
+
const current = timeSeries[timeSeries.length - 1].value;
|
|
5455
|
+
const h3 = Math.round(horizon / 3);
|
|
5456
|
+
const h2 = Math.round(horizon * 2 / 3);
|
|
5457
|
+
const projected4w = projectValue(fit, currentT + h3);
|
|
5458
|
+
const projected8w = projectValue(fit, currentT + h2);
|
|
5459
|
+
const projected12w = projectValue(fit, currentT + horizon);
|
|
5460
|
+
const crossing = weeksUntilThreshold(fit, currentT, threshold);
|
|
5461
|
+
const confidence = classifyConfidence(fit.rSquared, fit.dataPoints);
|
|
5462
|
+
const direction = this.classifyDirection(fit.slope);
|
|
5463
|
+
return {
|
|
5464
|
+
category,
|
|
5465
|
+
current,
|
|
5466
|
+
threshold,
|
|
5467
|
+
projectedValue4w: projected4w,
|
|
5468
|
+
projectedValue8w: projected8w,
|
|
5469
|
+
projectedValue12w: projected12w,
|
|
5470
|
+
thresholdCrossingWeeks: crossing,
|
|
5471
|
+
confidence,
|
|
5472
|
+
regression: {
|
|
5473
|
+
slope: fit.slope,
|
|
5474
|
+
intercept: fit.intercept,
|
|
5475
|
+
rSquared: fit.rSquared,
|
|
5476
|
+
dataPoints: fit.dataPoints
|
|
5477
|
+
},
|
|
5478
|
+
direction
|
|
5479
|
+
};
|
|
5480
|
+
}
|
|
5481
|
+
classifyDirection(slope) {
|
|
5482
|
+
if (Math.abs(slope) < DIRECTION_THRESHOLD) return "stable";
|
|
5483
|
+
return slope > 0 ? "declining" : "improving";
|
|
5484
|
+
}
|
|
5485
|
+
zeroForecast(category, threshold) {
|
|
5486
|
+
return {
|
|
5487
|
+
category,
|
|
5488
|
+
current: 0,
|
|
5489
|
+
threshold,
|
|
5490
|
+
projectedValue4w: 0,
|
|
5491
|
+
projectedValue8w: 0,
|
|
5492
|
+
projectedValue12w: 0,
|
|
5493
|
+
thresholdCrossingWeeks: null,
|
|
5494
|
+
confidence: "low",
|
|
5495
|
+
regression: { slope: 0, intercept: 0, rSquared: 0, dataPoints: 0 },
|
|
5496
|
+
direction: "stable"
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
/**
|
|
5500
|
+
* Generate warnings based on severity rules from spec:
|
|
5501
|
+
* - critical: threshold crossing <= 4w, confidence high or medium
|
|
5502
|
+
* - warning: threshold crossing <= 8w, confidence high or medium
|
|
5503
|
+
* - info: threshold crossing <= 12w, any confidence
|
|
5504
|
+
*/
|
|
5505
|
+
generateWarnings(categories, horizon = 12) {
|
|
5506
|
+
const warnings = [];
|
|
5507
|
+
const criticalWindow = Math.round(horizon / 3);
|
|
5508
|
+
const warningWindow = Math.round(horizon * 2 / 3);
|
|
5509
|
+
for (const category of ALL_CATEGORIES2) {
|
|
5510
|
+
const af = categories[category];
|
|
5511
|
+
if (!af) continue;
|
|
5512
|
+
const forecast = af.adjusted;
|
|
5513
|
+
const crossing = forecast.thresholdCrossingWeeks;
|
|
5514
|
+
if (crossing === null || crossing <= 0) continue;
|
|
5515
|
+
let severity = null;
|
|
5516
|
+
if (crossing <= criticalWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
|
|
5517
|
+
severity = "critical";
|
|
5518
|
+
} else if (crossing <= warningWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
|
|
5519
|
+
severity = "warning";
|
|
5520
|
+
} else if (crossing <= horizon) {
|
|
5521
|
+
severity = "info";
|
|
5522
|
+
}
|
|
5523
|
+
if (severity) {
|
|
5524
|
+
const contributingNames = af.contributingFeatures.map((f) => f.name);
|
|
5525
|
+
warnings.push({
|
|
5526
|
+
severity,
|
|
5527
|
+
category,
|
|
5528
|
+
message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
|
|
5529
|
+
weeksUntil: crossing,
|
|
5530
|
+
confidence: forecast.confidence,
|
|
5531
|
+
contributingFeatures: contributingNames
|
|
5532
|
+
});
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
return warnings;
|
|
5536
|
+
}
|
|
5537
|
+
/**
|
|
5538
|
+
* Compute composite stability forecast by projecting per-category values
|
|
5539
|
+
* forward and computing stability scores at each horizon.
|
|
5540
|
+
*/
|
|
5541
|
+
computeStabilityForecast(categories, thresholds, _snapshots) {
|
|
5542
|
+
const currentMetrics = this.buildMetricsFromForecasts(categories, "current");
|
|
5543
|
+
const current = this.timelineManager.computeStabilityScore(currentMetrics, thresholds);
|
|
5544
|
+
const metrics4w = this.buildMetricsFromForecasts(categories, "4w");
|
|
5545
|
+
const projected4w = this.timelineManager.computeStabilityScore(metrics4w, thresholds);
|
|
5546
|
+
const metrics8w = this.buildMetricsFromForecasts(categories, "8w");
|
|
5547
|
+
const projected8w = this.timelineManager.computeStabilityScore(metrics8w, thresholds);
|
|
5548
|
+
const metrics12w = this.buildMetricsFromForecasts(categories, "12w");
|
|
5549
|
+
const projected12w = this.timelineManager.computeStabilityScore(metrics12w, thresholds);
|
|
5550
|
+
const delta = projected12w - current;
|
|
5551
|
+
let direction;
|
|
5552
|
+
if (Math.abs(delta) < 2) {
|
|
5553
|
+
direction = "stable";
|
|
5554
|
+
} else {
|
|
5555
|
+
direction = delta > 0 ? "improving" : "declining";
|
|
5556
|
+
}
|
|
5557
|
+
const confidences = ALL_CATEGORIES2.map((c) => categories[c]?.adjusted.confidence ?? "low");
|
|
5558
|
+
const confidence = this.medianConfidence(confidences);
|
|
5559
|
+
return { current, projected4w, projected8w, projected12w, confidence, direction };
|
|
5560
|
+
}
|
|
5561
|
+
buildMetricsFromForecasts(categories, horizon) {
|
|
5562
|
+
const metrics = {};
|
|
5563
|
+
for (const cat of ALL_CATEGORIES2) {
|
|
5564
|
+
const forecast = categories[cat]?.adjusted;
|
|
5565
|
+
let value = 0;
|
|
5566
|
+
if (forecast) {
|
|
5567
|
+
switch (horizon) {
|
|
5568
|
+
case "current":
|
|
5569
|
+
value = forecast.current;
|
|
5570
|
+
break;
|
|
5571
|
+
case "4w":
|
|
5572
|
+
value = forecast.projectedValue4w;
|
|
5573
|
+
break;
|
|
5574
|
+
case "8w":
|
|
5575
|
+
value = forecast.projectedValue8w;
|
|
5576
|
+
break;
|
|
5577
|
+
case "12w":
|
|
5578
|
+
value = forecast.projectedValue12w;
|
|
5579
|
+
break;
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
metrics[cat] = { value: Math.max(0, value), violationCount: 0 };
|
|
5583
|
+
}
|
|
5584
|
+
return metrics;
|
|
5585
|
+
}
|
|
5586
|
+
medianConfidence(confidences) {
|
|
5587
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
5588
|
+
const sorted = [...confidences].sort((a, b) => order[a] - order[b]);
|
|
5589
|
+
const mid = Math.floor(sorted.length / 2);
|
|
5590
|
+
return sorted[mid] ?? "low";
|
|
5591
|
+
}
|
|
5592
|
+
/**
|
|
5593
|
+
* Load roadmap features, estimate spec impacts via the estimator.
|
|
5594
|
+
* Returns null if estimator is null or includeRoadmap is false.
|
|
5595
|
+
*/
|
|
5596
|
+
computeSpecImpacts(opts) {
|
|
5597
|
+
if (!this.estimator || !opts.includeRoadmap) {
|
|
5598
|
+
return null;
|
|
5599
|
+
}
|
|
5600
|
+
try {
|
|
5601
|
+
const roadmapPath = path2.join(this.rootDir, "roadmap.md");
|
|
5602
|
+
const raw = fs5.readFileSync(roadmapPath, "utf-8");
|
|
5603
|
+
const parseResult = parseRoadmap(raw);
|
|
5604
|
+
if (!parseResult.ok) return null;
|
|
5605
|
+
const features = [];
|
|
5606
|
+
for (const milestone of parseResult.value.milestones) {
|
|
5607
|
+
for (const feature of milestone.features) {
|
|
5608
|
+
if (feature.status === "planned" || feature.status === "in-progress") {
|
|
5609
|
+
features.push({ name: feature.name, spec: feature.spec });
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
if (features.length === 0) return null;
|
|
5614
|
+
return this.estimator.estimateAll(features);
|
|
5615
|
+
} catch {
|
|
5616
|
+
return null;
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
};
|
|
5620
|
+
|
|
5621
|
+
// src/architecture/spec-impact-estimator.ts
|
|
5622
|
+
import * as fs6 from "fs";
|
|
5623
|
+
import * as path3 from "path";
|
|
5624
|
+
var DEFAULT_COEFFICIENTS = {
|
|
5625
|
+
newFileModuleSize: 0.3,
|
|
5626
|
+
newFileComplexity: 1.5,
|
|
5627
|
+
layerViolation: 0.5,
|
|
5628
|
+
depCoupling: 0.2,
|
|
5629
|
+
depDepth: 0.3,
|
|
5630
|
+
phaseComplexity: 2
|
|
5631
|
+
};
|
|
5632
|
+
var SpecImpactEstimator = class {
|
|
5633
|
+
constructor(rootDir, coefficients) {
|
|
5634
|
+
this.rootDir = rootDir;
|
|
5635
|
+
this.coefficients = { ...DEFAULT_COEFFICIENTS, ...coefficients };
|
|
5636
|
+
this.layerNames = this.loadLayerNames();
|
|
5637
|
+
}
|
|
5638
|
+
rootDir;
|
|
5639
|
+
coefficients;
|
|
5640
|
+
layerNames;
|
|
5641
|
+
/**
|
|
5642
|
+
* Estimate impact of a single spec file.
|
|
5643
|
+
* @param specPath - Relative path from rootDir to the spec file.
|
|
5644
|
+
*/
|
|
5645
|
+
estimate(specPath) {
|
|
5646
|
+
const absolutePath = path3.join(this.rootDir, specPath);
|
|
5647
|
+
const content = fs6.readFileSync(absolutePath, "utf-8");
|
|
5648
|
+
const newFileCount = this.extractNewFileCount(content);
|
|
5649
|
+
const affectedLayers = this.extractAffectedLayers(content);
|
|
5650
|
+
const newDependencies = this.extractNewDependencies(content);
|
|
5651
|
+
const phaseCount = this.extractPhaseCount(content);
|
|
5652
|
+
const deltas = this.computeDeltas(
|
|
5653
|
+
newFileCount,
|
|
5654
|
+
affectedLayers.length,
|
|
5655
|
+
newDependencies,
|
|
5656
|
+
phaseCount
|
|
5657
|
+
);
|
|
5658
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
5659
|
+
const featureName = h1Match ? h1Match[1].trim() : path3.basename(specPath, ".md");
|
|
5660
|
+
return {
|
|
5661
|
+
specPath,
|
|
5662
|
+
featureName,
|
|
5663
|
+
signals: {
|
|
5664
|
+
newFileCount,
|
|
5665
|
+
affectedLayers,
|
|
5666
|
+
newDependencies,
|
|
5667
|
+
phaseCount
|
|
5668
|
+
},
|
|
5669
|
+
deltas
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* Estimate impact for all planned features that have specs.
|
|
5674
|
+
* Skips features with null specs or specs that don't exist on disk.
|
|
5675
|
+
*/
|
|
5676
|
+
estimateAll(features) {
|
|
5677
|
+
const results = [];
|
|
5678
|
+
for (const feature of features) {
|
|
5679
|
+
if (!feature.spec) continue;
|
|
5680
|
+
const absolutePath = path3.join(this.rootDir, feature.spec);
|
|
5681
|
+
if (!fs6.existsSync(absolutePath)) continue;
|
|
5682
|
+
const estimate = this.estimate(feature.spec);
|
|
5683
|
+
results.push({ ...estimate, featureName: feature.name });
|
|
5684
|
+
}
|
|
5685
|
+
return results;
|
|
5686
|
+
}
|
|
5687
|
+
// --- Private: Signal Extraction ---
|
|
5688
|
+
/**
|
|
5689
|
+
* Count file paths in Technical Design sections that don't exist on disk.
|
|
5690
|
+
* Looks for paths in code blocks (```) under ## Technical Design.
|
|
5691
|
+
*/
|
|
5692
|
+
extractNewFileCount(content) {
|
|
5693
|
+
const techDesignMatch = content.match(/## Technical Design\b[\s\S]*?(?=\n## |\n# |$)/i);
|
|
5694
|
+
if (!techDesignMatch) return 0;
|
|
5695
|
+
const section = techDesignMatch[0];
|
|
5696
|
+
const codeBlocks = section.match(/```[\s\S]*?```/g) ?? [];
|
|
5697
|
+
const filePaths = [];
|
|
5698
|
+
for (const block of codeBlocks) {
|
|
5699
|
+
const inner = block.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
|
|
5700
|
+
for (const line of inner.split("\n")) {
|
|
5701
|
+
const trimmed = line.trim();
|
|
5702
|
+
if (trimmed.match(/^[\w@.-]+\/[\w./-]+\.\w+$/)) {
|
|
5703
|
+
filePaths.push(trimmed);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
let count = 0;
|
|
5708
|
+
for (const fp of filePaths) {
|
|
5709
|
+
const absolute = path3.join(this.rootDir, fp);
|
|
5710
|
+
if (!fs6.existsSync(absolute)) {
|
|
5711
|
+
count++;
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
return count;
|
|
5715
|
+
}
|
|
5716
|
+
/**
|
|
5717
|
+
* Match layer names from harness.config.json mentioned in the spec.
|
|
5718
|
+
* Returns deduplicated array of matched layer names.
|
|
5719
|
+
*/
|
|
5720
|
+
extractAffectedLayers(content) {
|
|
5721
|
+
if (this.layerNames.length === 0) return [];
|
|
5722
|
+
const matched = /* @__PURE__ */ new Set();
|
|
5723
|
+
for (const layer of this.layerNames) {
|
|
5724
|
+
const pattern = new RegExp(`\\b${this.escapeRegex(layer)}\\b`, "i");
|
|
5725
|
+
if (pattern.test(content)) {
|
|
5726
|
+
matched.add(layer);
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
return [...matched].sort();
|
|
5730
|
+
}
|
|
5731
|
+
/**
|
|
5732
|
+
* Count dependency-related keywords: "import", "depend" (covers depends/dependency),
|
|
5733
|
+
* "package" in dependency context.
|
|
5734
|
+
*/
|
|
5735
|
+
extractNewDependencies(content) {
|
|
5736
|
+
const patterns = [/\bimport\b/gi, /\bdepend\w*\b/gi, /\bpackage\b/gi];
|
|
5737
|
+
let count = 0;
|
|
5738
|
+
for (const pattern of patterns) {
|
|
5739
|
+
const matches = content.match(pattern);
|
|
5740
|
+
if (matches) count += matches.length;
|
|
5741
|
+
}
|
|
5742
|
+
return count;
|
|
5743
|
+
}
|
|
5744
|
+
/**
|
|
5745
|
+
* Count H3/H4 headings under "Implementation" or "Implementation Order" sections.
|
|
5746
|
+
*/
|
|
5747
|
+
extractPhaseCount(content) {
|
|
5748
|
+
const implMatch = content.match(/## Implementation\b[\s\S]*?(?=\n## |\n# |$)/i);
|
|
5749
|
+
if (!implMatch) return 0;
|
|
5750
|
+
const section = implMatch[0];
|
|
5751
|
+
const headings = section.match(/^#{3,4}\s+.+$/gm);
|
|
5752
|
+
return headings ? headings.length : 0;
|
|
5753
|
+
}
|
|
5754
|
+
// --- Private: Delta Computation ---
|
|
5755
|
+
computeDeltas(newFileCount, crossLayerCount, newDependencies, phaseCount) {
|
|
5756
|
+
const deltas = {};
|
|
5757
|
+
const c = this.coefficients;
|
|
5758
|
+
const addDelta = (category, value) => {
|
|
5759
|
+
deltas[category] = (deltas[category] ?? 0) + value;
|
|
5760
|
+
};
|
|
5761
|
+
if (newFileCount > 0) {
|
|
5762
|
+
addDelta("module-size", newFileCount * c.newFileModuleSize);
|
|
5763
|
+
addDelta("complexity", newFileCount * c.newFileComplexity);
|
|
5764
|
+
}
|
|
5765
|
+
if (crossLayerCount > 0) {
|
|
5766
|
+
addDelta("layer-violations", crossLayerCount * c.layerViolation);
|
|
5767
|
+
}
|
|
5768
|
+
if (newDependencies > 0) {
|
|
5769
|
+
addDelta("coupling", newDependencies * c.depCoupling);
|
|
5770
|
+
addDelta("dependency-depth", newDependencies * c.depDepth);
|
|
5771
|
+
}
|
|
5772
|
+
if (phaseCount > 1) {
|
|
5773
|
+
addDelta("complexity", (phaseCount - 1) * c.phaseComplexity);
|
|
5774
|
+
}
|
|
5775
|
+
return deltas;
|
|
5776
|
+
}
|
|
5777
|
+
// --- Private: Config Loading ---
|
|
5778
|
+
loadLayerNames() {
|
|
5779
|
+
try {
|
|
5780
|
+
const configPath = path3.join(this.rootDir, "harness.config.json");
|
|
5781
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
5782
|
+
const config = JSON.parse(raw);
|
|
5783
|
+
return (config.layers ?? []).map((l) => l.name);
|
|
5784
|
+
} catch {
|
|
5785
|
+
return [];
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
escapeRegex(str) {
|
|
5789
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5790
|
+
}
|
|
5791
|
+
};
|
|
5792
|
+
|
|
5793
|
+
// src/state/types.ts
|
|
5794
|
+
import { z as z5 } from "zod";
|
|
5795
|
+
var FailureEntrySchema = z5.object({
|
|
5796
|
+
date: z5.string(),
|
|
5797
|
+
skill: z5.string(),
|
|
5798
|
+
type: z5.string(),
|
|
5799
|
+
description: z5.string()
|
|
5800
|
+
});
|
|
5801
|
+
var HandoffSchema = z5.object({
|
|
5802
|
+
timestamp: z5.string(),
|
|
5803
|
+
fromSkill: z5.string(),
|
|
5804
|
+
phase: z5.string(),
|
|
5805
|
+
summary: z5.string(),
|
|
5806
|
+
completed: z5.array(z5.string()).default([]),
|
|
5807
|
+
pending: z5.array(z5.string()).default([]),
|
|
5808
|
+
concerns: z5.array(z5.string()).default([]),
|
|
5809
|
+
decisions: z5.array(
|
|
5810
|
+
z5.object({
|
|
5811
|
+
what: z5.string(),
|
|
5812
|
+
why: z5.string()
|
|
4717
5813
|
})
|
|
4718
5814
|
).default([]),
|
|
4719
|
-
blockers:
|
|
4720
|
-
contextKeywords:
|
|
5815
|
+
blockers: z5.array(z5.string()).default([]),
|
|
5816
|
+
contextKeywords: z5.array(z5.string()).default([])
|
|
4721
5817
|
});
|
|
4722
|
-
var GateCheckSchema =
|
|
4723
|
-
name:
|
|
4724
|
-
passed:
|
|
4725
|
-
command:
|
|
4726
|
-
output:
|
|
4727
|
-
duration:
|
|
5818
|
+
var GateCheckSchema = z5.object({
|
|
5819
|
+
name: z5.string(),
|
|
5820
|
+
passed: z5.boolean(),
|
|
5821
|
+
command: z5.string(),
|
|
5822
|
+
output: z5.string().optional(),
|
|
5823
|
+
duration: z5.number().optional()
|
|
4728
5824
|
});
|
|
4729
|
-
var GateResultSchema =
|
|
4730
|
-
passed:
|
|
4731
|
-
checks:
|
|
5825
|
+
var GateResultSchema = z5.object({
|
|
5826
|
+
passed: z5.boolean(),
|
|
5827
|
+
checks: z5.array(GateCheckSchema)
|
|
4732
5828
|
});
|
|
4733
|
-
var GateConfigSchema =
|
|
4734
|
-
checks:
|
|
4735
|
-
|
|
4736
|
-
name:
|
|
4737
|
-
command:
|
|
5829
|
+
var GateConfigSchema = z5.object({
|
|
5830
|
+
checks: z5.array(
|
|
5831
|
+
z5.object({
|
|
5832
|
+
name: z5.string(),
|
|
5833
|
+
command: z5.string()
|
|
4738
5834
|
})
|
|
4739
5835
|
).optional(),
|
|
4740
|
-
trace:
|
|
5836
|
+
trace: z5.boolean().optional()
|
|
4741
5837
|
});
|
|
4742
|
-
var HarnessStateSchema =
|
|
4743
|
-
schemaVersion:
|
|
4744
|
-
position:
|
|
4745
|
-
phase:
|
|
4746
|
-
task:
|
|
5838
|
+
var HarnessStateSchema = z5.object({
|
|
5839
|
+
schemaVersion: z5.literal(1),
|
|
5840
|
+
position: z5.object({
|
|
5841
|
+
phase: z5.string().optional(),
|
|
5842
|
+
task: z5.string().optional()
|
|
4747
5843
|
}).default({}),
|
|
4748
|
-
decisions:
|
|
4749
|
-
|
|
4750
|
-
date:
|
|
4751
|
-
decision:
|
|
4752
|
-
context:
|
|
5844
|
+
decisions: z5.array(
|
|
5845
|
+
z5.object({
|
|
5846
|
+
date: z5.string(),
|
|
5847
|
+
decision: z5.string(),
|
|
5848
|
+
context: z5.string()
|
|
4753
5849
|
})
|
|
4754
5850
|
).default([]),
|
|
4755
|
-
blockers:
|
|
4756
|
-
|
|
4757
|
-
id:
|
|
4758
|
-
description:
|
|
4759
|
-
status:
|
|
5851
|
+
blockers: z5.array(
|
|
5852
|
+
z5.object({
|
|
5853
|
+
id: z5.string(),
|
|
5854
|
+
description: z5.string(),
|
|
5855
|
+
status: z5.enum(["open", "resolved"])
|
|
4760
5856
|
})
|
|
4761
5857
|
).default([]),
|
|
4762
|
-
progress:
|
|
4763
|
-
lastSession:
|
|
4764
|
-
date:
|
|
4765
|
-
summary:
|
|
4766
|
-
lastSkill:
|
|
4767
|
-
pendingTasks:
|
|
5858
|
+
progress: z5.record(z5.enum(["pending", "in_progress", "complete"])).default({}),
|
|
5859
|
+
lastSession: z5.object({
|
|
5860
|
+
date: z5.string(),
|
|
5861
|
+
summary: z5.string(),
|
|
5862
|
+
lastSkill: z5.string().optional(),
|
|
5863
|
+
pendingTasks: z5.array(z5.string()).optional()
|
|
4768
5864
|
}).optional()
|
|
4769
5865
|
});
|
|
4770
5866
|
var DEFAULT_STATE = {
|
|
@@ -4776,30 +5872,30 @@ var DEFAULT_STATE = {
|
|
|
4776
5872
|
};
|
|
4777
5873
|
|
|
4778
5874
|
// src/state/state-persistence.ts
|
|
4779
|
-
import * as
|
|
4780
|
-
import * as
|
|
5875
|
+
import * as fs10 from "fs";
|
|
5876
|
+
import * as path7 from "path";
|
|
4781
5877
|
|
|
4782
5878
|
// src/state/state-shared.ts
|
|
4783
|
-
import * as
|
|
4784
|
-
import * as
|
|
5879
|
+
import * as fs9 from "fs";
|
|
5880
|
+
import * as path6 from "path";
|
|
4785
5881
|
|
|
4786
5882
|
// src/state/stream-resolver.ts
|
|
4787
|
-
import * as
|
|
4788
|
-
import * as
|
|
5883
|
+
import * as fs7 from "fs";
|
|
5884
|
+
import * as path4 from "path";
|
|
4789
5885
|
import { execSync } from "child_process";
|
|
4790
5886
|
|
|
4791
5887
|
// src/state/stream-types.ts
|
|
4792
|
-
import { z as
|
|
4793
|
-
var StreamInfoSchema =
|
|
4794
|
-
name:
|
|
4795
|
-
branch:
|
|
4796
|
-
createdAt:
|
|
4797
|
-
lastActiveAt:
|
|
5888
|
+
import { z as z6 } from "zod";
|
|
5889
|
+
var StreamInfoSchema = z6.object({
|
|
5890
|
+
name: z6.string(),
|
|
5891
|
+
branch: z6.string().optional(),
|
|
5892
|
+
createdAt: z6.string(),
|
|
5893
|
+
lastActiveAt: z6.string()
|
|
4798
5894
|
});
|
|
4799
|
-
var StreamIndexSchema =
|
|
4800
|
-
schemaVersion:
|
|
4801
|
-
activeStream:
|
|
4802
|
-
streams:
|
|
5895
|
+
var StreamIndexSchema = z6.object({
|
|
5896
|
+
schemaVersion: z6.literal(1),
|
|
5897
|
+
activeStream: z6.string().nullable(),
|
|
5898
|
+
streams: z6.record(StreamInfoSchema)
|
|
4803
5899
|
});
|
|
4804
5900
|
var DEFAULT_STREAM_INDEX = {
|
|
4805
5901
|
schemaVersion: 1,
|
|
@@ -4827,10 +5923,10 @@ var EVENTS_FILE = "events.jsonl";
|
|
|
4827
5923
|
var STREAMS_DIR = "streams";
|
|
4828
5924
|
var STREAM_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
|
|
4829
5925
|
function streamsDir(projectPath) {
|
|
4830
|
-
return
|
|
5926
|
+
return path4.join(projectPath, HARNESS_DIR, STREAMS_DIR);
|
|
4831
5927
|
}
|
|
4832
5928
|
function indexPath(projectPath) {
|
|
4833
|
-
return
|
|
5929
|
+
return path4.join(streamsDir(projectPath), INDEX_FILE);
|
|
4834
5930
|
}
|
|
4835
5931
|
function validateStreamName(name) {
|
|
4836
5932
|
if (!STREAM_NAME_REGEX.test(name)) {
|
|
@@ -4844,11 +5940,11 @@ function validateStreamName(name) {
|
|
|
4844
5940
|
}
|
|
4845
5941
|
async function loadStreamIndex(projectPath) {
|
|
4846
5942
|
const idxPath = indexPath(projectPath);
|
|
4847
|
-
if (!
|
|
5943
|
+
if (!fs7.existsSync(idxPath)) {
|
|
4848
5944
|
return Ok({ ...DEFAULT_STREAM_INDEX, streams: {} });
|
|
4849
5945
|
}
|
|
4850
5946
|
try {
|
|
4851
|
-
const raw =
|
|
5947
|
+
const raw = fs7.readFileSync(idxPath, "utf-8");
|
|
4852
5948
|
const parsed = JSON.parse(raw);
|
|
4853
5949
|
const result = StreamIndexSchema.safeParse(parsed);
|
|
4854
5950
|
if (!result.success) {
|
|
@@ -4866,8 +5962,8 @@ async function loadStreamIndex(projectPath) {
|
|
|
4866
5962
|
async function saveStreamIndex(projectPath, index) {
|
|
4867
5963
|
const dir = streamsDir(projectPath);
|
|
4868
5964
|
try {
|
|
4869
|
-
|
|
4870
|
-
|
|
5965
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
5966
|
+
fs7.writeFileSync(indexPath(projectPath), JSON.stringify(index, null, 2));
|
|
4871
5967
|
return Ok(void 0);
|
|
4872
5968
|
} catch (error) {
|
|
4873
5969
|
return Err(
|
|
@@ -4908,18 +6004,18 @@ async function resolveStreamPath(projectPath, options) {
|
|
|
4908
6004
|
)
|
|
4909
6005
|
);
|
|
4910
6006
|
}
|
|
4911
|
-
return Ok(
|
|
6007
|
+
return Ok(path4.join(streamsDir(projectPath), options.stream));
|
|
4912
6008
|
}
|
|
4913
6009
|
const branch = getCurrentBranch(projectPath);
|
|
4914
6010
|
if (branch && branch !== "main" && branch !== "master") {
|
|
4915
6011
|
for (const [name, info] of Object.entries(index.streams)) {
|
|
4916
6012
|
if (info.branch === branch) {
|
|
4917
|
-
return Ok(
|
|
6013
|
+
return Ok(path4.join(streamsDir(projectPath), name));
|
|
4918
6014
|
}
|
|
4919
6015
|
}
|
|
4920
6016
|
}
|
|
4921
6017
|
if (index.activeStream && index.streams[index.activeStream]) {
|
|
4922
|
-
return Ok(
|
|
6018
|
+
return Ok(path4.join(streamsDir(projectPath), index.activeStream));
|
|
4923
6019
|
}
|
|
4924
6020
|
return Err(
|
|
4925
6021
|
new Error(
|
|
@@ -4947,9 +6043,9 @@ async function createStream(projectPath, name, branch) {
|
|
|
4947
6043
|
if (index.streams[name]) {
|
|
4948
6044
|
return Err(new Error(`Stream '${name}' already exists`));
|
|
4949
6045
|
}
|
|
4950
|
-
const streamPath =
|
|
6046
|
+
const streamPath = path4.join(streamsDir(projectPath), name);
|
|
4951
6047
|
try {
|
|
4952
|
-
|
|
6048
|
+
fs7.mkdirSync(streamPath, { recursive: true });
|
|
4953
6049
|
} catch (error) {
|
|
4954
6050
|
return Err(
|
|
4955
6051
|
new Error(
|
|
@@ -4990,12 +6086,12 @@ async function archiveStream(projectPath, name) {
|
|
|
4990
6086
|
if (!index.streams[name]) {
|
|
4991
6087
|
return Err(new Error(`Stream '${name}' not found`));
|
|
4992
6088
|
}
|
|
4993
|
-
const streamPath =
|
|
4994
|
-
const archiveDir =
|
|
6089
|
+
const streamPath = path4.join(streamsDir(projectPath), name);
|
|
6090
|
+
const archiveDir = path4.join(projectPath, HARNESS_DIR, "archive", "streams");
|
|
4995
6091
|
try {
|
|
4996
|
-
|
|
6092
|
+
fs7.mkdirSync(archiveDir, { recursive: true });
|
|
4997
6093
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4998
|
-
|
|
6094
|
+
fs7.renameSync(streamPath, path4.join(archiveDir, `${name}-${date}`));
|
|
4999
6095
|
} catch (error) {
|
|
5000
6096
|
return Err(
|
|
5001
6097
|
new Error(
|
|
@@ -5017,19 +6113,19 @@ function getStreamForBranch(index, branch) {
|
|
|
5017
6113
|
}
|
|
5018
6114
|
var STATE_FILES = ["state.json", "handoff.json", "learnings.md", "failures.md"];
|
|
5019
6115
|
async function migrateToStreams(projectPath) {
|
|
5020
|
-
const harnessDir =
|
|
5021
|
-
if (
|
|
6116
|
+
const harnessDir = path4.join(projectPath, HARNESS_DIR);
|
|
6117
|
+
if (fs7.existsSync(indexPath(projectPath))) {
|
|
5022
6118
|
return Ok(void 0);
|
|
5023
6119
|
}
|
|
5024
|
-
const filesToMove = STATE_FILES.filter((f) =>
|
|
6120
|
+
const filesToMove = STATE_FILES.filter((f) => fs7.existsSync(path4.join(harnessDir, f)));
|
|
5025
6121
|
if (filesToMove.length === 0) {
|
|
5026
6122
|
return Ok(void 0);
|
|
5027
6123
|
}
|
|
5028
|
-
const defaultDir =
|
|
6124
|
+
const defaultDir = path4.join(streamsDir(projectPath), "default");
|
|
5029
6125
|
try {
|
|
5030
|
-
|
|
6126
|
+
fs7.mkdirSync(defaultDir, { recursive: true });
|
|
5031
6127
|
for (const file of filesToMove) {
|
|
5032
|
-
|
|
6128
|
+
fs7.renameSync(path4.join(harnessDir, file), path4.join(defaultDir, file));
|
|
5033
6129
|
}
|
|
5034
6130
|
} catch (error) {
|
|
5035
6131
|
return Err(
|
|
@@ -5052,8 +6148,8 @@ async function migrateToStreams(projectPath) {
|
|
|
5052
6148
|
}
|
|
5053
6149
|
|
|
5054
6150
|
// src/state/session-resolver.ts
|
|
5055
|
-
import * as
|
|
5056
|
-
import * as
|
|
6151
|
+
import * as fs8 from "fs";
|
|
6152
|
+
import * as path5 from "path";
|
|
5057
6153
|
function resolveSessionDir(projectPath, sessionSlug, options) {
|
|
5058
6154
|
if (!sessionSlug || sessionSlug.trim() === "") {
|
|
5059
6155
|
return Err(new Error("Session slug must not be empty"));
|
|
@@ -5063,26 +6159,26 @@ function resolveSessionDir(projectPath, sessionSlug, options) {
|
|
|
5063
6159
|
new Error(`Invalid session slug '${sessionSlug}': must not contain path traversal characters`)
|
|
5064
6160
|
);
|
|
5065
6161
|
}
|
|
5066
|
-
const sessionDir =
|
|
6162
|
+
const sessionDir = path5.join(projectPath, HARNESS_DIR, SESSIONS_DIR, sessionSlug);
|
|
5067
6163
|
if (options?.create) {
|
|
5068
|
-
|
|
6164
|
+
fs8.mkdirSync(sessionDir, { recursive: true });
|
|
5069
6165
|
}
|
|
5070
6166
|
return Ok(sessionDir);
|
|
5071
6167
|
}
|
|
5072
6168
|
function updateSessionIndex(projectPath, sessionSlug, description) {
|
|
5073
|
-
const sessionsDir =
|
|
5074
|
-
|
|
5075
|
-
const indexPath2 =
|
|
6169
|
+
const sessionsDir = path5.join(projectPath, HARNESS_DIR, SESSIONS_DIR);
|
|
6170
|
+
fs8.mkdirSync(sessionsDir, { recursive: true });
|
|
6171
|
+
const indexPath2 = path5.join(sessionsDir, SESSION_INDEX_FILE);
|
|
5076
6172
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5077
6173
|
const newLine = `- [${sessionSlug}](${sessionSlug}/summary.md) \u2014 ${description} (${date})`;
|
|
5078
|
-
if (!
|
|
5079
|
-
|
|
6174
|
+
if (!fs8.existsSync(indexPath2)) {
|
|
6175
|
+
fs8.writeFileSync(indexPath2, `## Active Sessions
|
|
5080
6176
|
|
|
5081
6177
|
${newLine}
|
|
5082
6178
|
`);
|
|
5083
6179
|
return;
|
|
5084
6180
|
}
|
|
5085
|
-
const content =
|
|
6181
|
+
const content = fs8.readFileSync(indexPath2, "utf-8");
|
|
5086
6182
|
const lines = content.split("\n");
|
|
5087
6183
|
const slugPattern = `- [${sessionSlug}]`;
|
|
5088
6184
|
const existingIdx = lines.findIndex((l) => l.startsWith(slugPattern));
|
|
@@ -5092,7 +6188,7 @@ ${newLine}
|
|
|
5092
6188
|
const lastNonEmpty = lines.reduce((last, line, i) => line.trim() !== "" ? i : last, 0);
|
|
5093
6189
|
lines.splice(lastNonEmpty + 1, 0, newLine);
|
|
5094
6190
|
}
|
|
5095
|
-
|
|
6191
|
+
fs8.writeFileSync(indexPath2, lines.join("\n"));
|
|
5096
6192
|
}
|
|
5097
6193
|
|
|
5098
6194
|
// src/state/state-shared.ts
|
|
@@ -5108,8 +6204,8 @@ async function getStateDir(projectPath, stream, session) {
|
|
|
5108
6204
|
const sessionResult = resolveSessionDir(projectPath, session, { create: true });
|
|
5109
6205
|
return sessionResult;
|
|
5110
6206
|
}
|
|
5111
|
-
const streamsIndexPath =
|
|
5112
|
-
const hasStreams =
|
|
6207
|
+
const streamsIndexPath = path6.join(projectPath, HARNESS_DIR, "streams", INDEX_FILE);
|
|
6208
|
+
const hasStreams = fs9.existsSync(streamsIndexPath);
|
|
5113
6209
|
if (stream || hasStreams) {
|
|
5114
6210
|
const result = await resolveStreamPath(projectPath, stream ? { stream } : void 0);
|
|
5115
6211
|
if (result.ok) {
|
|
@@ -5119,7 +6215,7 @@ async function getStateDir(projectPath, stream, session) {
|
|
|
5119
6215
|
return result;
|
|
5120
6216
|
}
|
|
5121
6217
|
}
|
|
5122
|
-
return Ok(
|
|
6218
|
+
return Ok(path6.join(projectPath, HARNESS_DIR));
|
|
5123
6219
|
}
|
|
5124
6220
|
|
|
5125
6221
|
// src/state/state-persistence.ts
|
|
@@ -5128,11 +6224,11 @@ async function loadState(projectPath, stream, session) {
|
|
|
5128
6224
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5129
6225
|
if (!dirResult.ok) return dirResult;
|
|
5130
6226
|
const stateDir = dirResult.value;
|
|
5131
|
-
const statePath =
|
|
5132
|
-
if (!
|
|
6227
|
+
const statePath = path7.join(stateDir, STATE_FILE);
|
|
6228
|
+
if (!fs10.existsSync(statePath)) {
|
|
5133
6229
|
return Ok({ ...DEFAULT_STATE });
|
|
5134
6230
|
}
|
|
5135
|
-
const raw =
|
|
6231
|
+
const raw = fs10.readFileSync(statePath, "utf-8");
|
|
5136
6232
|
const parsed = JSON.parse(raw);
|
|
5137
6233
|
const result = HarnessStateSchema.safeParse(parsed);
|
|
5138
6234
|
if (!result.success) {
|
|
@@ -5150,9 +6246,9 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
5150
6246
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5151
6247
|
if (!dirResult.ok) return dirResult;
|
|
5152
6248
|
const stateDir = dirResult.value;
|
|
5153
|
-
const statePath =
|
|
5154
|
-
|
|
5155
|
-
|
|
6249
|
+
const statePath = path7.join(stateDir, STATE_FILE);
|
|
6250
|
+
fs10.mkdirSync(stateDir, { recursive: true });
|
|
6251
|
+
fs10.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
5156
6252
|
return Ok(void 0);
|
|
5157
6253
|
} catch (error) {
|
|
5158
6254
|
return Err(
|
|
@@ -5162,10 +6258,10 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
5162
6258
|
}
|
|
5163
6259
|
|
|
5164
6260
|
// src/state/learnings.ts
|
|
5165
|
-
import * as
|
|
5166
|
-
import * as
|
|
6261
|
+
import * as fs11 from "fs";
|
|
6262
|
+
import * as path8 from "path";
|
|
5167
6263
|
import * as crypto from "crypto";
|
|
5168
|
-
function
|
|
6264
|
+
function parseFrontmatter2(line) {
|
|
5169
6265
|
const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
|
|
5170
6266
|
if (!match) return null;
|
|
5171
6267
|
const hash = match[1];
|
|
@@ -5191,10 +6287,10 @@ function computeContentHash(text) {
|
|
|
5191
6287
|
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
5192
6288
|
}
|
|
5193
6289
|
function loadContentHashes(stateDir) {
|
|
5194
|
-
const hashesPath =
|
|
5195
|
-
if (!
|
|
6290
|
+
const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
|
|
6291
|
+
if (!fs11.existsSync(hashesPath)) return {};
|
|
5196
6292
|
try {
|
|
5197
|
-
const raw =
|
|
6293
|
+
const raw = fs11.readFileSync(hashesPath, "utf-8");
|
|
5198
6294
|
const parsed = JSON.parse(raw);
|
|
5199
6295
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
5200
6296
|
return parsed;
|
|
@@ -5203,13 +6299,13 @@ function loadContentHashes(stateDir) {
|
|
|
5203
6299
|
}
|
|
5204
6300
|
}
|
|
5205
6301
|
function saveContentHashes(stateDir, index) {
|
|
5206
|
-
const hashesPath =
|
|
5207
|
-
|
|
6302
|
+
const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
|
|
6303
|
+
fs11.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
|
|
5208
6304
|
}
|
|
5209
6305
|
function rebuildContentHashes(stateDir) {
|
|
5210
|
-
const learningsPath =
|
|
5211
|
-
if (!
|
|
5212
|
-
const content =
|
|
6306
|
+
const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6307
|
+
if (!fs11.existsSync(learningsPath)) return {};
|
|
6308
|
+
const content = fs11.readFileSync(learningsPath, "utf-8");
|
|
5213
6309
|
const lines = content.split("\n");
|
|
5214
6310
|
const index = {};
|
|
5215
6311
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -5252,18 +6348,18 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
5252
6348
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5253
6349
|
if (!dirResult.ok) return dirResult;
|
|
5254
6350
|
const stateDir = dirResult.value;
|
|
5255
|
-
const learningsPath =
|
|
5256
|
-
|
|
6351
|
+
const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6352
|
+
fs11.mkdirSync(stateDir, { recursive: true });
|
|
5257
6353
|
const normalizedContent = normalizeLearningContent(learning);
|
|
5258
6354
|
const contentHash = computeContentHash(normalizedContent);
|
|
5259
|
-
const hashesPath =
|
|
6355
|
+
const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
|
|
5260
6356
|
let contentHashes;
|
|
5261
|
-
if (
|
|
6357
|
+
if (fs11.existsSync(hashesPath)) {
|
|
5262
6358
|
contentHashes = loadContentHashes(stateDir);
|
|
5263
|
-
if (Object.keys(contentHashes).length === 0 &&
|
|
6359
|
+
if (Object.keys(contentHashes).length === 0 && fs11.existsSync(learningsPath)) {
|
|
5264
6360
|
contentHashes = rebuildContentHashes(stateDir);
|
|
5265
6361
|
}
|
|
5266
|
-
} else if (
|
|
6362
|
+
} else if (fs11.existsSync(learningsPath)) {
|
|
5267
6363
|
contentHashes = rebuildContentHashes(stateDir);
|
|
5268
6364
|
} else {
|
|
5269
6365
|
contentHashes = {};
|
|
@@ -5291,14 +6387,14 @@ ${frontmatter}
|
|
|
5291
6387
|
${bulletLine}
|
|
5292
6388
|
`;
|
|
5293
6389
|
let existingLineCount;
|
|
5294
|
-
if (!
|
|
5295
|
-
|
|
6390
|
+
if (!fs11.existsSync(learningsPath)) {
|
|
6391
|
+
fs11.writeFileSync(learningsPath, `# Learnings
|
|
5296
6392
|
${entry}`);
|
|
5297
6393
|
existingLineCount = 1;
|
|
5298
6394
|
} else {
|
|
5299
|
-
const existingContent =
|
|
6395
|
+
const existingContent = fs11.readFileSync(learningsPath, "utf-8");
|
|
5300
6396
|
existingLineCount = existingContent.split("\n").length;
|
|
5301
|
-
|
|
6397
|
+
fs11.appendFileSync(learningsPath, entry);
|
|
5302
6398
|
}
|
|
5303
6399
|
const bulletLine_lineNum = existingLineCount + 2;
|
|
5304
6400
|
contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLine_lineNum };
|
|
@@ -5412,18 +6508,18 @@ async function loadIndexEntries(projectPath, skillName, stream, session) {
|
|
|
5412
6508
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5413
6509
|
if (!dirResult.ok) return dirResult;
|
|
5414
6510
|
const stateDir = dirResult.value;
|
|
5415
|
-
const learningsPath =
|
|
5416
|
-
if (!
|
|
6511
|
+
const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6512
|
+
if (!fs11.existsSync(learningsPath)) {
|
|
5417
6513
|
return Ok([]);
|
|
5418
6514
|
}
|
|
5419
|
-
const content =
|
|
6515
|
+
const content = fs11.readFileSync(learningsPath, "utf-8");
|
|
5420
6516
|
const lines = content.split("\n");
|
|
5421
6517
|
const indexEntries = [];
|
|
5422
6518
|
let pendingFrontmatter = null;
|
|
5423
6519
|
let currentBlock = [];
|
|
5424
6520
|
for (const line of lines) {
|
|
5425
6521
|
if (line.startsWith("# ")) continue;
|
|
5426
|
-
const fm =
|
|
6522
|
+
const fm = parseFrontmatter2(line);
|
|
5427
6523
|
if (fm) {
|
|
5428
6524
|
pendingFrontmatter = fm;
|
|
5429
6525
|
continue;
|
|
@@ -5474,18 +6570,18 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
5474
6570
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5475
6571
|
if (!dirResult.ok) return dirResult;
|
|
5476
6572
|
const stateDir = dirResult.value;
|
|
5477
|
-
const learningsPath =
|
|
5478
|
-
if (!
|
|
6573
|
+
const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6574
|
+
if (!fs11.existsSync(learningsPath)) {
|
|
5479
6575
|
return Ok([]);
|
|
5480
6576
|
}
|
|
5481
|
-
const stats =
|
|
6577
|
+
const stats = fs11.statSync(learningsPath);
|
|
5482
6578
|
const cacheKey = learningsPath;
|
|
5483
6579
|
const cached = learningsCacheMap.get(cacheKey);
|
|
5484
6580
|
let entries;
|
|
5485
6581
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
5486
6582
|
entries = cached.entries;
|
|
5487
6583
|
} else {
|
|
5488
|
-
const content =
|
|
6584
|
+
const content = fs11.readFileSync(learningsPath, "utf-8");
|
|
5489
6585
|
const lines = content.split("\n");
|
|
5490
6586
|
entries = [];
|
|
5491
6587
|
let currentBlock = [];
|
|
@@ -5527,16 +6623,16 @@ async function archiveLearnings(projectPath, entries, stream) {
|
|
|
5527
6623
|
const dirResult = await getStateDir(projectPath, stream);
|
|
5528
6624
|
if (!dirResult.ok) return dirResult;
|
|
5529
6625
|
const stateDir = dirResult.value;
|
|
5530
|
-
const archiveDir =
|
|
5531
|
-
|
|
6626
|
+
const archiveDir = path8.join(stateDir, "learnings-archive");
|
|
6627
|
+
fs11.mkdirSync(archiveDir, { recursive: true });
|
|
5532
6628
|
const now = /* @__PURE__ */ new Date();
|
|
5533
6629
|
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
5534
|
-
const archivePath =
|
|
6630
|
+
const archivePath = path8.join(archiveDir, `${yearMonth}.md`);
|
|
5535
6631
|
const archiveContent = entries.join("\n\n") + "\n";
|
|
5536
|
-
if (
|
|
5537
|
-
|
|
6632
|
+
if (fs11.existsSync(archivePath)) {
|
|
6633
|
+
fs11.appendFileSync(archivePath, "\n" + archiveContent);
|
|
5538
6634
|
} else {
|
|
5539
|
-
|
|
6635
|
+
fs11.writeFileSync(archivePath, `# Learnings Archive
|
|
5540
6636
|
|
|
5541
6637
|
${archiveContent}`);
|
|
5542
6638
|
}
|
|
@@ -5554,8 +6650,8 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
5554
6650
|
const dirResult = await getStateDir(projectPath, stream);
|
|
5555
6651
|
if (!dirResult.ok) return dirResult;
|
|
5556
6652
|
const stateDir = dirResult.value;
|
|
5557
|
-
const learningsPath =
|
|
5558
|
-
if (!
|
|
6653
|
+
const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6654
|
+
if (!fs11.existsSync(learningsPath)) {
|
|
5559
6655
|
return Ok({ kept: 0, archived: 0, patterns: [] });
|
|
5560
6656
|
}
|
|
5561
6657
|
const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
|
|
@@ -5586,7 +6682,7 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
5586
6682
|
if (!archiveResult.ok) return archiveResult;
|
|
5587
6683
|
}
|
|
5588
6684
|
const newContent = "# Learnings\n\n" + toKeep.join("\n\n") + "\n";
|
|
5589
|
-
|
|
6685
|
+
fs11.writeFileSync(learningsPath, newContent);
|
|
5590
6686
|
learningsCacheMap.delete(learningsPath);
|
|
5591
6687
|
return Ok({
|
|
5592
6688
|
kept: toKeep.length,
|
|
@@ -5631,19 +6727,19 @@ async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
|
|
|
5631
6727
|
const dirResult = await getStateDir(projectPath, stream);
|
|
5632
6728
|
if (!dirResult.ok) return dirResult;
|
|
5633
6729
|
const stateDir = dirResult.value;
|
|
5634
|
-
const globalPath =
|
|
5635
|
-
const existingGlobal =
|
|
6730
|
+
const globalPath = path8.join(stateDir, LEARNINGS_FILE);
|
|
6731
|
+
const existingGlobal = fs11.existsSync(globalPath) ? fs11.readFileSync(globalPath, "utf-8") : "";
|
|
5636
6732
|
const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
|
|
5637
6733
|
if (newEntries.length === 0) {
|
|
5638
6734
|
return Ok({ promoted: 0, skipped: skipped + toPromote.length });
|
|
5639
6735
|
}
|
|
5640
6736
|
const promotedContent = newEntries.join("\n\n") + "\n";
|
|
5641
6737
|
if (!existingGlobal) {
|
|
5642
|
-
|
|
6738
|
+
fs11.writeFileSync(globalPath, `# Learnings
|
|
5643
6739
|
|
|
5644
6740
|
${promotedContent}`);
|
|
5645
6741
|
} else {
|
|
5646
|
-
|
|
6742
|
+
fs11.appendFileSync(globalPath, "\n\n" + promotedContent);
|
|
5647
6743
|
}
|
|
5648
6744
|
learningsCacheMap.delete(globalPath);
|
|
5649
6745
|
return Ok({
|
|
@@ -5665,8 +6761,8 @@ async function countLearningEntries(projectPath, stream) {
|
|
|
5665
6761
|
}
|
|
5666
6762
|
|
|
5667
6763
|
// src/state/failures.ts
|
|
5668
|
-
import * as
|
|
5669
|
-
import * as
|
|
6764
|
+
import * as fs12 from "fs";
|
|
6765
|
+
import * as path9 from "path";
|
|
5670
6766
|
var failuresCacheMap = /* @__PURE__ */ new Map();
|
|
5671
6767
|
function clearFailuresCache() {
|
|
5672
6768
|
failuresCacheMap.clear();
|
|
@@ -5677,17 +6773,17 @@ async function appendFailure(projectPath, description, skillName, type, stream,
|
|
|
5677
6773
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5678
6774
|
if (!dirResult.ok) return dirResult;
|
|
5679
6775
|
const stateDir = dirResult.value;
|
|
5680
|
-
const failuresPath =
|
|
5681
|
-
|
|
6776
|
+
const failuresPath = path9.join(stateDir, FAILURES_FILE);
|
|
6777
|
+
fs12.mkdirSync(stateDir, { recursive: true });
|
|
5682
6778
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5683
6779
|
const entry = `
|
|
5684
6780
|
- **${timestamp} [skill:${skillName}] [type:${type}]:** ${description}
|
|
5685
6781
|
`;
|
|
5686
|
-
if (!
|
|
5687
|
-
|
|
6782
|
+
if (!fs12.existsSync(failuresPath)) {
|
|
6783
|
+
fs12.writeFileSync(failuresPath, `# Failures
|
|
5688
6784
|
${entry}`);
|
|
5689
6785
|
} else {
|
|
5690
|
-
|
|
6786
|
+
fs12.appendFileSync(failuresPath, entry);
|
|
5691
6787
|
}
|
|
5692
6788
|
failuresCacheMap.delete(failuresPath);
|
|
5693
6789
|
return Ok(void 0);
|
|
@@ -5704,17 +6800,17 @@ async function loadFailures(projectPath, stream, session) {
|
|
|
5704
6800
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5705
6801
|
if (!dirResult.ok) return dirResult;
|
|
5706
6802
|
const stateDir = dirResult.value;
|
|
5707
|
-
const failuresPath =
|
|
5708
|
-
if (!
|
|
6803
|
+
const failuresPath = path9.join(stateDir, FAILURES_FILE);
|
|
6804
|
+
if (!fs12.existsSync(failuresPath)) {
|
|
5709
6805
|
return Ok([]);
|
|
5710
6806
|
}
|
|
5711
|
-
const stats =
|
|
6807
|
+
const stats = fs12.statSync(failuresPath);
|
|
5712
6808
|
const cacheKey = failuresPath;
|
|
5713
6809
|
const cached = failuresCacheMap.get(cacheKey);
|
|
5714
6810
|
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
5715
6811
|
return Ok(cached.entries);
|
|
5716
6812
|
}
|
|
5717
|
-
const content =
|
|
6813
|
+
const content = fs12.readFileSync(failuresPath, "utf-8");
|
|
5718
6814
|
const entries = [];
|
|
5719
6815
|
for (const line of content.split("\n")) {
|
|
5720
6816
|
const match = line.match(FAILURE_LINE_REGEX);
|
|
@@ -5743,20 +6839,20 @@ async function archiveFailures(projectPath, stream, session) {
|
|
|
5743
6839
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5744
6840
|
if (!dirResult.ok) return dirResult;
|
|
5745
6841
|
const stateDir = dirResult.value;
|
|
5746
|
-
const failuresPath =
|
|
5747
|
-
if (!
|
|
6842
|
+
const failuresPath = path9.join(stateDir, FAILURES_FILE);
|
|
6843
|
+
if (!fs12.existsSync(failuresPath)) {
|
|
5748
6844
|
return Ok(void 0);
|
|
5749
6845
|
}
|
|
5750
|
-
const archiveDir =
|
|
5751
|
-
|
|
6846
|
+
const archiveDir = path9.join(stateDir, "archive");
|
|
6847
|
+
fs12.mkdirSync(archiveDir, { recursive: true });
|
|
5752
6848
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5753
6849
|
let archiveName = `failures-${date}.md`;
|
|
5754
6850
|
let counter = 2;
|
|
5755
|
-
while (
|
|
6851
|
+
while (fs12.existsSync(path9.join(archiveDir, archiveName))) {
|
|
5756
6852
|
archiveName = `failures-${date}-${counter}.md`;
|
|
5757
6853
|
counter++;
|
|
5758
6854
|
}
|
|
5759
|
-
|
|
6855
|
+
fs12.renameSync(failuresPath, path9.join(archiveDir, archiveName));
|
|
5760
6856
|
failuresCacheMap.delete(failuresPath);
|
|
5761
6857
|
return Ok(void 0);
|
|
5762
6858
|
} catch (error) {
|
|
@@ -5769,16 +6865,16 @@ async function archiveFailures(projectPath, stream, session) {
|
|
|
5769
6865
|
}
|
|
5770
6866
|
|
|
5771
6867
|
// src/state/handoff.ts
|
|
5772
|
-
import * as
|
|
5773
|
-
import * as
|
|
6868
|
+
import * as fs13 from "fs";
|
|
6869
|
+
import * as path10 from "path";
|
|
5774
6870
|
async function saveHandoff(projectPath, handoff, stream, session) {
|
|
5775
6871
|
try {
|
|
5776
6872
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5777
6873
|
if (!dirResult.ok) return dirResult;
|
|
5778
6874
|
const stateDir = dirResult.value;
|
|
5779
|
-
const handoffPath =
|
|
5780
|
-
|
|
5781
|
-
|
|
6875
|
+
const handoffPath = path10.join(stateDir, HANDOFF_FILE);
|
|
6876
|
+
fs13.mkdirSync(stateDir, { recursive: true });
|
|
6877
|
+
fs13.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
|
|
5782
6878
|
return Ok(void 0);
|
|
5783
6879
|
} catch (error) {
|
|
5784
6880
|
return Err(
|
|
@@ -5791,11 +6887,11 @@ async function loadHandoff(projectPath, stream, session) {
|
|
|
5791
6887
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5792
6888
|
if (!dirResult.ok) return dirResult;
|
|
5793
6889
|
const stateDir = dirResult.value;
|
|
5794
|
-
const handoffPath =
|
|
5795
|
-
if (!
|
|
6890
|
+
const handoffPath = path10.join(stateDir, HANDOFF_FILE);
|
|
6891
|
+
if (!fs13.existsSync(handoffPath)) {
|
|
5796
6892
|
return Ok(null);
|
|
5797
6893
|
}
|
|
5798
|
-
const raw =
|
|
6894
|
+
const raw = fs13.readFileSync(handoffPath, "utf-8");
|
|
5799
6895
|
const parsed = JSON.parse(raw);
|
|
5800
6896
|
const result = HandoffSchema.safeParse(parsed);
|
|
5801
6897
|
if (!result.success) {
|
|
@@ -5810,33 +6906,33 @@ async function loadHandoff(projectPath, stream, session) {
|
|
|
5810
6906
|
}
|
|
5811
6907
|
|
|
5812
6908
|
// src/state/mechanical-gate.ts
|
|
5813
|
-
import * as
|
|
5814
|
-
import * as
|
|
6909
|
+
import * as fs14 from "fs";
|
|
6910
|
+
import * as path11 from "path";
|
|
5815
6911
|
import { execSync as execSync2 } from "child_process";
|
|
5816
6912
|
var SAFE_GATE_COMMAND = /^(?:npm|pnpm|yarn)\s+(?:test|run\s+[\w.-]+|run-script\s+[\w.-]+)$|^go\s+(?:test|build|vet|fmt)\s+[\w./ -]+$|^(?:python|python3)\s+-m\s+[\w.-]+$|^make\s+[\w.-]+$|^cargo\s+(?:test|build|check|clippy)(?:\s+[\w./ -]+)?$|^(?:gradle|mvn)\s+[\w:.-]+$/;
|
|
5817
6913
|
function loadChecksFromConfig(gateConfigPath) {
|
|
5818
|
-
if (!
|
|
5819
|
-
const raw = JSON.parse(
|
|
6914
|
+
if (!fs14.existsSync(gateConfigPath)) return [];
|
|
6915
|
+
const raw = JSON.parse(fs14.readFileSync(gateConfigPath, "utf-8"));
|
|
5820
6916
|
const config = GateConfigSchema.safeParse(raw);
|
|
5821
6917
|
if (config.success && config.data.checks) return config.data.checks;
|
|
5822
6918
|
return [];
|
|
5823
6919
|
}
|
|
5824
6920
|
function discoverChecksFromProject(projectPath) {
|
|
5825
6921
|
const checks = [];
|
|
5826
|
-
const packageJsonPath =
|
|
5827
|
-
if (
|
|
5828
|
-
const pkg = JSON.parse(
|
|
6922
|
+
const packageJsonPath = path11.join(projectPath, "package.json");
|
|
6923
|
+
if (fs14.existsSync(packageJsonPath)) {
|
|
6924
|
+
const pkg = JSON.parse(fs14.readFileSync(packageJsonPath, "utf-8"));
|
|
5829
6925
|
const scripts = pkg.scripts || {};
|
|
5830
6926
|
if (scripts.test) checks.push({ name: "test", command: "npm test" });
|
|
5831
6927
|
if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
|
|
5832
6928
|
if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
|
|
5833
6929
|
if (scripts.build) checks.push({ name: "build", command: "npm run build" });
|
|
5834
6930
|
}
|
|
5835
|
-
if (
|
|
6931
|
+
if (fs14.existsSync(path11.join(projectPath, "go.mod"))) {
|
|
5836
6932
|
checks.push({ name: "test", command: "go test ./..." });
|
|
5837
6933
|
checks.push({ name: "build", command: "go build ./..." });
|
|
5838
6934
|
}
|
|
5839
|
-
if (
|
|
6935
|
+
if (fs14.existsSync(path11.join(projectPath, "pyproject.toml")) || fs14.existsSync(path11.join(projectPath, "setup.py"))) {
|
|
5840
6936
|
checks.push({ name: "test", command: "python -m pytest" });
|
|
5841
6937
|
}
|
|
5842
6938
|
return checks;
|
|
@@ -5876,8 +6972,8 @@ function executeCheck(check, projectPath) {
|
|
|
5876
6972
|
}
|
|
5877
6973
|
}
|
|
5878
6974
|
async function runMechanicalGate(projectPath) {
|
|
5879
|
-
const harnessDir =
|
|
5880
|
-
const gateConfigPath =
|
|
6975
|
+
const harnessDir = path11.join(projectPath, HARNESS_DIR);
|
|
6976
|
+
const gateConfigPath = path11.join(harnessDir, GATE_CONFIG_FILE);
|
|
5881
6977
|
try {
|
|
5882
6978
|
let checks = loadChecksFromConfig(gateConfigPath);
|
|
5883
6979
|
if (checks.length === 0) {
|
|
@@ -5898,8 +6994,8 @@ async function runMechanicalGate(projectPath) {
|
|
|
5898
6994
|
}
|
|
5899
6995
|
|
|
5900
6996
|
// src/state/session-summary.ts
|
|
5901
|
-
import * as
|
|
5902
|
-
import * as
|
|
6997
|
+
import * as fs15 from "fs";
|
|
6998
|
+
import * as path12 from "path";
|
|
5903
6999
|
function formatSummary(data) {
|
|
5904
7000
|
const lines = [
|
|
5905
7001
|
"## Session Summary",
|
|
@@ -5937,9 +7033,9 @@ function writeSessionSummary(projectPath, sessionSlug, data) {
|
|
|
5937
7033
|
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
5938
7034
|
if (!dirResult.ok) return dirResult;
|
|
5939
7035
|
const sessionDir = dirResult.value;
|
|
5940
|
-
const summaryPath =
|
|
7036
|
+
const summaryPath = path12.join(sessionDir, SUMMARY_FILE);
|
|
5941
7037
|
const content = formatSummary(data);
|
|
5942
|
-
|
|
7038
|
+
fs15.writeFileSync(summaryPath, content);
|
|
5943
7039
|
const description = deriveIndexDescription(data);
|
|
5944
7040
|
updateSessionIndex(projectPath, sessionSlug, description);
|
|
5945
7041
|
return Ok(void 0);
|
|
@@ -5956,11 +7052,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
|
|
|
5956
7052
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
5957
7053
|
if (!dirResult.ok) return dirResult;
|
|
5958
7054
|
const sessionDir = dirResult.value;
|
|
5959
|
-
const summaryPath =
|
|
5960
|
-
if (!
|
|
7055
|
+
const summaryPath = path12.join(sessionDir, SUMMARY_FILE);
|
|
7056
|
+
if (!fs15.existsSync(summaryPath)) {
|
|
5961
7057
|
return Ok(null);
|
|
5962
7058
|
}
|
|
5963
|
-
const content =
|
|
7059
|
+
const content = fs15.readFileSync(summaryPath, "utf-8");
|
|
5964
7060
|
return Ok(content);
|
|
5965
7061
|
} catch (error) {
|
|
5966
7062
|
return Err(
|
|
@@ -5972,11 +7068,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
|
|
|
5972
7068
|
}
|
|
5973
7069
|
function listActiveSessions(projectPath) {
|
|
5974
7070
|
try {
|
|
5975
|
-
const indexPath2 =
|
|
5976
|
-
if (!
|
|
7071
|
+
const indexPath2 = path12.join(projectPath, HARNESS_DIR, SESSIONS_DIR, SESSION_INDEX_FILE);
|
|
7072
|
+
if (!fs15.existsSync(indexPath2)) {
|
|
5977
7073
|
return Ok(null);
|
|
5978
7074
|
}
|
|
5979
|
-
const content =
|
|
7075
|
+
const content = fs15.readFileSync(indexPath2, "utf-8");
|
|
5980
7076
|
return Ok(content);
|
|
5981
7077
|
} catch (error) {
|
|
5982
7078
|
return Err(
|
|
@@ -5988,8 +7084,8 @@ function listActiveSessions(projectPath) {
|
|
|
5988
7084
|
}
|
|
5989
7085
|
|
|
5990
7086
|
// src/state/session-sections.ts
|
|
5991
|
-
import * as
|
|
5992
|
-
import * as
|
|
7087
|
+
import * as fs16 from "fs";
|
|
7088
|
+
import * as path13 from "path";
|
|
5993
7089
|
import { SESSION_SECTION_NAMES } from "@harness-engineering/types";
|
|
5994
7090
|
function emptySections() {
|
|
5995
7091
|
const sections = {};
|
|
@@ -6002,12 +7098,12 @@ async function loadSessionState(projectPath, sessionSlug) {
|
|
|
6002
7098
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
6003
7099
|
if (!dirResult.ok) return dirResult;
|
|
6004
7100
|
const sessionDir = dirResult.value;
|
|
6005
|
-
const filePath =
|
|
6006
|
-
if (!
|
|
7101
|
+
const filePath = path13.join(sessionDir, SESSION_STATE_FILE);
|
|
7102
|
+
if (!fs16.existsSync(filePath)) {
|
|
6007
7103
|
return Ok(emptySections());
|
|
6008
7104
|
}
|
|
6009
7105
|
try {
|
|
6010
|
-
const raw =
|
|
7106
|
+
const raw = fs16.readFileSync(filePath, "utf-8");
|
|
6011
7107
|
const parsed = JSON.parse(raw);
|
|
6012
7108
|
const sections = emptySections();
|
|
6013
7109
|
for (const name of SESSION_SECTION_NAMES) {
|
|
@@ -6028,9 +7124,9 @@ async function saveSessionState(projectPath, sessionSlug, sections) {
|
|
|
6028
7124
|
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
6029
7125
|
if (!dirResult.ok) return dirResult;
|
|
6030
7126
|
const sessionDir = dirResult.value;
|
|
6031
|
-
const filePath =
|
|
7127
|
+
const filePath = path13.join(sessionDir, SESSION_STATE_FILE);
|
|
6032
7128
|
try {
|
|
6033
|
-
|
|
7129
|
+
fs16.writeFileSync(filePath, JSON.stringify(sections, null, 2));
|
|
6034
7130
|
return Ok(void 0);
|
|
6035
7131
|
} catch (error) {
|
|
6036
7132
|
return Err(
|
|
@@ -6083,33 +7179,33 @@ function generateEntryId() {
|
|
|
6083
7179
|
return `${timestamp}-${random}`;
|
|
6084
7180
|
}
|
|
6085
7181
|
|
|
6086
|
-
// src/state/session-archive.ts
|
|
6087
|
-
import * as
|
|
6088
|
-
import * as
|
|
7182
|
+
// src/state/session-archive.ts
|
|
7183
|
+
import * as fs17 from "fs";
|
|
7184
|
+
import * as path14 from "path";
|
|
6089
7185
|
async function archiveSession(projectPath, sessionSlug) {
|
|
6090
7186
|
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
6091
7187
|
if (!dirResult.ok) return dirResult;
|
|
6092
7188
|
const sessionDir = dirResult.value;
|
|
6093
|
-
if (!
|
|
7189
|
+
if (!fs17.existsSync(sessionDir)) {
|
|
6094
7190
|
return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
|
|
6095
7191
|
}
|
|
6096
|
-
const archiveBase =
|
|
7192
|
+
const archiveBase = path14.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
|
|
6097
7193
|
try {
|
|
6098
|
-
|
|
7194
|
+
fs17.mkdirSync(archiveBase, { recursive: true });
|
|
6099
7195
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
6100
7196
|
let archiveName = `${sessionSlug}-${date}`;
|
|
6101
7197
|
let counter = 1;
|
|
6102
|
-
while (
|
|
7198
|
+
while (fs17.existsSync(path14.join(archiveBase, archiveName))) {
|
|
6103
7199
|
archiveName = `${sessionSlug}-${date}-${counter}`;
|
|
6104
7200
|
counter++;
|
|
6105
7201
|
}
|
|
6106
|
-
const dest =
|
|
7202
|
+
const dest = path14.join(archiveBase, archiveName);
|
|
6107
7203
|
try {
|
|
6108
|
-
|
|
7204
|
+
fs17.renameSync(sessionDir, dest);
|
|
6109
7205
|
} catch (renameErr) {
|
|
6110
7206
|
if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
|
|
6111
|
-
|
|
6112
|
-
|
|
7207
|
+
fs17.cpSync(sessionDir, dest, { recursive: true });
|
|
7208
|
+
fs17.rmSync(sessionDir, { recursive: true });
|
|
6113
7209
|
} else {
|
|
6114
7210
|
throw renameErr;
|
|
6115
7211
|
}
|
|
@@ -6125,18 +7221,18 @@ async function archiveSession(projectPath, sessionSlug) {
|
|
|
6125
7221
|
}
|
|
6126
7222
|
|
|
6127
7223
|
// src/state/events.ts
|
|
6128
|
-
import * as
|
|
6129
|
-
import * as
|
|
6130
|
-
import { z as
|
|
6131
|
-
var SkillEventSchema =
|
|
6132
|
-
timestamp:
|
|
6133
|
-
skill:
|
|
6134
|
-
session:
|
|
6135
|
-
type:
|
|
6136
|
-
summary:
|
|
6137
|
-
data:
|
|
6138
|
-
refs:
|
|
6139
|
-
contentHash:
|
|
7224
|
+
import * as fs18 from "fs";
|
|
7225
|
+
import * as path15 from "path";
|
|
7226
|
+
import { z as z7 } from "zod";
|
|
7227
|
+
var SkillEventSchema = z7.object({
|
|
7228
|
+
timestamp: z7.string(),
|
|
7229
|
+
skill: z7.string(),
|
|
7230
|
+
session: z7.string().optional(),
|
|
7231
|
+
type: z7.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
|
|
7232
|
+
summary: z7.string(),
|
|
7233
|
+
data: z7.record(z7.unknown()).optional(),
|
|
7234
|
+
refs: z7.array(z7.string()).optional(),
|
|
7235
|
+
contentHash: z7.string().optional()
|
|
6140
7236
|
});
|
|
6141
7237
|
function computeEventHash(event, session) {
|
|
6142
7238
|
const identity = `${event.skill}|${event.type}|${event.summary}|${session ?? ""}`;
|
|
@@ -6147,8 +7243,8 @@ function loadKnownHashes(eventsPath) {
|
|
|
6147
7243
|
const cached = knownHashesCache.get(eventsPath);
|
|
6148
7244
|
if (cached) return cached;
|
|
6149
7245
|
const hashes = /* @__PURE__ */ new Set();
|
|
6150
|
-
if (
|
|
6151
|
-
const content =
|
|
7246
|
+
if (fs18.existsSync(eventsPath)) {
|
|
7247
|
+
const content = fs18.readFileSync(eventsPath, "utf-8");
|
|
6152
7248
|
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
6153
7249
|
for (const line of lines) {
|
|
6154
7250
|
try {
|
|
@@ -6171,8 +7267,8 @@ async function emitEvent(projectPath, event, options) {
|
|
|
6171
7267
|
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
6172
7268
|
if (!dirResult.ok) return dirResult;
|
|
6173
7269
|
const stateDir = dirResult.value;
|
|
6174
|
-
const eventsPath =
|
|
6175
|
-
|
|
7270
|
+
const eventsPath = path15.join(stateDir, EVENTS_FILE);
|
|
7271
|
+
fs18.mkdirSync(stateDir, { recursive: true });
|
|
6176
7272
|
const contentHash = computeEventHash(event, options?.session);
|
|
6177
7273
|
const knownHashes = loadKnownHashes(eventsPath);
|
|
6178
7274
|
if (knownHashes.has(contentHash)) {
|
|
@@ -6186,7 +7282,7 @@ async function emitEvent(projectPath, event, options) {
|
|
|
6186
7282
|
if (options?.session) {
|
|
6187
7283
|
fullEvent.session = options.session;
|
|
6188
7284
|
}
|
|
6189
|
-
|
|
7285
|
+
fs18.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
|
|
6190
7286
|
knownHashes.add(contentHash);
|
|
6191
7287
|
return Ok({ written: true });
|
|
6192
7288
|
} catch (error) {
|
|
@@ -6200,11 +7296,11 @@ async function loadEvents(projectPath, options) {
|
|
|
6200
7296
|
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
6201
7297
|
if (!dirResult.ok) return dirResult;
|
|
6202
7298
|
const stateDir = dirResult.value;
|
|
6203
|
-
const eventsPath =
|
|
6204
|
-
if (!
|
|
7299
|
+
const eventsPath = path15.join(stateDir, EVENTS_FILE);
|
|
7300
|
+
if (!fs18.existsSync(eventsPath)) {
|
|
6205
7301
|
return Ok([]);
|
|
6206
7302
|
}
|
|
6207
|
-
const content =
|
|
7303
|
+
const content = fs18.readFileSync(eventsPath, "utf-8");
|
|
6208
7304
|
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
6209
7305
|
const events = [];
|
|
6210
7306
|
for (const line of lines) {
|
|
@@ -6418,7 +7514,7 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
6418
7514
|
}
|
|
6419
7515
|
|
|
6420
7516
|
// src/security/scanner.ts
|
|
6421
|
-
import * as
|
|
7517
|
+
import * as fs20 from "fs/promises";
|
|
6422
7518
|
import { minimatch as minimatch4 } from "minimatch";
|
|
6423
7519
|
|
|
6424
7520
|
// src/security/rules/registry.ts
|
|
@@ -6450,7 +7546,7 @@ var RuleRegistry = class {
|
|
|
6450
7546
|
};
|
|
6451
7547
|
|
|
6452
7548
|
// src/security/config.ts
|
|
6453
|
-
import { z as
|
|
7549
|
+
import { z as z8 } from "zod";
|
|
6454
7550
|
|
|
6455
7551
|
// src/security/types.ts
|
|
6456
7552
|
var DEFAULT_SECURITY_CONFIG = {
|
|
@@ -6461,19 +7557,19 @@ var DEFAULT_SECURITY_CONFIG = {
|
|
|
6461
7557
|
};
|
|
6462
7558
|
|
|
6463
7559
|
// src/security/config.ts
|
|
6464
|
-
var RuleOverrideSchema =
|
|
6465
|
-
var SecurityConfigSchema =
|
|
6466
|
-
enabled:
|
|
6467
|
-
strict:
|
|
6468
|
-
rules:
|
|
6469
|
-
exclude:
|
|
6470
|
-
external:
|
|
6471
|
-
semgrep:
|
|
6472
|
-
enabled:
|
|
6473
|
-
rulesets:
|
|
7560
|
+
var RuleOverrideSchema = z8.enum(["off", "error", "warning", "info"]);
|
|
7561
|
+
var SecurityConfigSchema = z8.object({
|
|
7562
|
+
enabled: z8.boolean().default(true),
|
|
7563
|
+
strict: z8.boolean().default(false),
|
|
7564
|
+
rules: z8.record(z8.string(), RuleOverrideSchema).optional().default({}),
|
|
7565
|
+
exclude: z8.array(z8.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
|
|
7566
|
+
external: z8.object({
|
|
7567
|
+
semgrep: z8.object({
|
|
7568
|
+
enabled: z8.union([z8.literal("auto"), z8.boolean()]).default("auto"),
|
|
7569
|
+
rulesets: z8.array(z8.string()).optional()
|
|
6474
7570
|
}).optional(),
|
|
6475
|
-
gitleaks:
|
|
6476
|
-
enabled:
|
|
7571
|
+
gitleaks: z8.object({
|
|
7572
|
+
enabled: z8.union([z8.literal("auto"), z8.boolean()]).default("auto")
|
|
6477
7573
|
}).optional()
|
|
6478
7574
|
}).optional()
|
|
6479
7575
|
});
|
|
@@ -6506,15 +7602,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
6506
7602
|
}
|
|
6507
7603
|
|
|
6508
7604
|
// src/security/stack-detector.ts
|
|
6509
|
-
import * as
|
|
6510
|
-
import * as
|
|
7605
|
+
import * as fs19 from "fs";
|
|
7606
|
+
import * as path16 from "path";
|
|
6511
7607
|
function detectStack(projectRoot) {
|
|
6512
7608
|
const stacks = [];
|
|
6513
|
-
const pkgJsonPath =
|
|
6514
|
-
if (
|
|
7609
|
+
const pkgJsonPath = path16.join(projectRoot, "package.json");
|
|
7610
|
+
if (fs19.existsSync(pkgJsonPath)) {
|
|
6515
7611
|
stacks.push("node");
|
|
6516
7612
|
try {
|
|
6517
|
-
const pkgJson = JSON.parse(
|
|
7613
|
+
const pkgJson = JSON.parse(fs19.readFileSync(pkgJsonPath, "utf-8"));
|
|
6518
7614
|
const allDeps = {
|
|
6519
7615
|
...pkgJson.dependencies,
|
|
6520
7616
|
...pkgJson.devDependencies
|
|
@@ -6529,13 +7625,13 @@ function detectStack(projectRoot) {
|
|
|
6529
7625
|
} catch {
|
|
6530
7626
|
}
|
|
6531
7627
|
}
|
|
6532
|
-
const goModPath =
|
|
6533
|
-
if (
|
|
7628
|
+
const goModPath = path16.join(projectRoot, "go.mod");
|
|
7629
|
+
if (fs19.existsSync(goModPath)) {
|
|
6534
7630
|
stacks.push("go");
|
|
6535
7631
|
}
|
|
6536
|
-
const requirementsPath =
|
|
6537
|
-
const pyprojectPath =
|
|
6538
|
-
if (
|
|
7632
|
+
const requirementsPath = path16.join(projectRoot, "requirements.txt");
|
|
7633
|
+
const pyprojectPath = path16.join(projectRoot, "pyproject.toml");
|
|
7634
|
+
if (fs19.existsSync(requirementsPath) || fs19.existsSync(pyprojectPath)) {
|
|
6539
7635
|
stacks.push("python");
|
|
6540
7636
|
}
|
|
6541
7637
|
return stacks;
|
|
@@ -7366,7 +8462,7 @@ var SecurityScanner = class {
|
|
|
7366
8462
|
}
|
|
7367
8463
|
async scanFile(filePath) {
|
|
7368
8464
|
if (!this.config.enabled) return [];
|
|
7369
|
-
const content = await
|
|
8465
|
+
const content = await fs20.readFile(filePath, "utf-8");
|
|
7370
8466
|
return this.scanContentForFile(content, filePath, 1);
|
|
7371
8467
|
}
|
|
7372
8468
|
scanContentForFile(content, filePath, startLine = 1) {
|
|
@@ -7693,19 +8789,19 @@ var DESTRUCTIVE_BASH = [
|
|
|
7693
8789
|
];
|
|
7694
8790
|
|
|
7695
8791
|
// src/security/taint.ts
|
|
7696
|
-
import { readFileSync as
|
|
7697
|
-
import { join as
|
|
8792
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync, mkdirSync as mkdirSync12, readdirSync as readdirSync3 } from "fs";
|
|
8793
|
+
import { join as join24, dirname as dirname9 } from "path";
|
|
7698
8794
|
var TAINT_DURATION_MS = 30 * 60 * 1e3;
|
|
7699
8795
|
var DEFAULT_SESSION_ID = "default";
|
|
7700
8796
|
function getTaintFilePath(projectRoot, sessionId) {
|
|
7701
8797
|
const id = sessionId || DEFAULT_SESSION_ID;
|
|
7702
|
-
return
|
|
8798
|
+
return join24(projectRoot, ".harness", `session-taint-${id}.json`);
|
|
7703
8799
|
}
|
|
7704
8800
|
function readTaint(projectRoot, sessionId) {
|
|
7705
8801
|
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
7706
8802
|
let content;
|
|
7707
8803
|
try {
|
|
7708
|
-
content =
|
|
8804
|
+
content = readFileSync17(filePath, "utf8");
|
|
7709
8805
|
} catch {
|
|
7710
8806
|
return null;
|
|
7711
8807
|
}
|
|
@@ -7749,8 +8845,8 @@ function writeTaint(projectRoot, sessionId, reason, findings, source) {
|
|
|
7749
8845
|
const id = sessionId || DEFAULT_SESSION_ID;
|
|
7750
8846
|
const filePath = getTaintFilePath(projectRoot, id);
|
|
7751
8847
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7752
|
-
const dir =
|
|
7753
|
-
|
|
8848
|
+
const dir = dirname9(filePath);
|
|
8849
|
+
mkdirSync12(dir, { recursive: true });
|
|
7754
8850
|
const existing = readTaint(projectRoot, id);
|
|
7755
8851
|
const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
|
|
7756
8852
|
const taintFindings = findings.map((f) => ({
|
|
@@ -7768,7 +8864,7 @@ function writeTaint(projectRoot, sessionId, reason, findings, source) {
|
|
|
7768
8864
|
severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
|
|
7769
8865
|
findings: [...existing?.findings || [], ...taintFindings]
|
|
7770
8866
|
};
|
|
7771
|
-
|
|
8867
|
+
writeFileSync12(filePath, JSON.stringify(state, null, 2) + "\n");
|
|
7772
8868
|
return state;
|
|
7773
8869
|
}
|
|
7774
8870
|
function clearTaint(projectRoot, sessionId) {
|
|
@@ -7781,14 +8877,14 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
7781
8877
|
return 0;
|
|
7782
8878
|
}
|
|
7783
8879
|
}
|
|
7784
|
-
const harnessDir =
|
|
8880
|
+
const harnessDir = join24(projectRoot, ".harness");
|
|
7785
8881
|
let count = 0;
|
|
7786
8882
|
try {
|
|
7787
8883
|
const files = readdirSync3(harnessDir);
|
|
7788
8884
|
for (const file of files) {
|
|
7789
8885
|
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
7790
8886
|
try {
|
|
7791
|
-
unlinkSync(
|
|
8887
|
+
unlinkSync(join24(harnessDir, file));
|
|
7792
8888
|
count++;
|
|
7793
8889
|
} catch {
|
|
7794
8890
|
}
|
|
@@ -7799,7 +8895,7 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
7799
8895
|
return count;
|
|
7800
8896
|
}
|
|
7801
8897
|
function listTaintedSessions(projectRoot) {
|
|
7802
|
-
const harnessDir =
|
|
8898
|
+
const harnessDir = join24(projectRoot, ".harness");
|
|
7803
8899
|
const sessions = [];
|
|
7804
8900
|
try {
|
|
7805
8901
|
const files = readdirSync3(harnessDir);
|
|
@@ -7869,7 +8965,8 @@ function mapSecurityFindings(secFindings, existing) {
|
|
|
7869
8965
|
}
|
|
7870
8966
|
|
|
7871
8967
|
// src/ci/check-orchestrator.ts
|
|
7872
|
-
import * as
|
|
8968
|
+
import * as path17 from "path";
|
|
8969
|
+
import { GraphStore, queryTraceability } from "@harness-engineering/graph";
|
|
7873
8970
|
var ALL_CHECKS = [
|
|
7874
8971
|
"validate",
|
|
7875
8972
|
"deps",
|
|
@@ -7878,11 +8975,12 @@ var ALL_CHECKS = [
|
|
|
7878
8975
|
"security",
|
|
7879
8976
|
"perf",
|
|
7880
8977
|
"phase-gate",
|
|
7881
|
-
"arch"
|
|
8978
|
+
"arch",
|
|
8979
|
+
"traceability"
|
|
7882
8980
|
];
|
|
7883
8981
|
async function runValidateCheck(projectRoot, config) {
|
|
7884
8982
|
const issues = [];
|
|
7885
|
-
const agentsPath =
|
|
8983
|
+
const agentsPath = path17.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
7886
8984
|
const result = await validateAgentsMap(agentsPath);
|
|
7887
8985
|
if (!result.ok) {
|
|
7888
8986
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -7939,7 +9037,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
7939
9037
|
}
|
|
7940
9038
|
async function runDocsCheck(projectRoot, config) {
|
|
7941
9039
|
const issues = [];
|
|
7942
|
-
const docsDir =
|
|
9040
|
+
const docsDir = path17.join(projectRoot, config.docsDir ?? "docs");
|
|
7943
9041
|
const entropyConfig = config.entropy || {};
|
|
7944
9042
|
const result = await checkDocCoverage("project", {
|
|
7945
9043
|
docsDir,
|
|
@@ -8120,6 +9218,39 @@ async function runArchCheck(projectRoot, config) {
|
|
|
8120
9218
|
}
|
|
8121
9219
|
return issues;
|
|
8122
9220
|
}
|
|
9221
|
+
async function runTraceabilityCheck(projectRoot, config) {
|
|
9222
|
+
const issues = [];
|
|
9223
|
+
const traceConfig = config.traceability || {};
|
|
9224
|
+
if (traceConfig.enabled === false) return issues;
|
|
9225
|
+
const graphDir = path17.join(projectRoot, ".harness", "graph");
|
|
9226
|
+
const store = new GraphStore();
|
|
9227
|
+
const loaded = await store.load(graphDir);
|
|
9228
|
+
if (!loaded) {
|
|
9229
|
+
return issues;
|
|
9230
|
+
}
|
|
9231
|
+
const results = queryTraceability(store);
|
|
9232
|
+
if (results.length === 0) return issues;
|
|
9233
|
+
const minCoverage = traceConfig.minCoverage ?? 0;
|
|
9234
|
+
const severity = traceConfig.severity ?? "warning";
|
|
9235
|
+
for (const result of results) {
|
|
9236
|
+
const pct = result.summary.coveragePercent;
|
|
9237
|
+
if (pct < minCoverage) {
|
|
9238
|
+
issues.push({
|
|
9239
|
+
severity,
|
|
9240
|
+
message: `Traceability coverage for "${result.featureName}" is ${pct}% (minimum: ${minCoverage}%)`
|
|
9241
|
+
});
|
|
9242
|
+
}
|
|
9243
|
+
for (const req of result.requirements) {
|
|
9244
|
+
if (req.status === "none") {
|
|
9245
|
+
issues.push({
|
|
9246
|
+
severity: "warning",
|
|
9247
|
+
message: `Requirement "${req.requirementName}" has no traced code or tests`
|
|
9248
|
+
});
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
return issues;
|
|
9253
|
+
}
|
|
8123
9254
|
async function runSingleCheck(name, projectRoot, config) {
|
|
8124
9255
|
const start = Date.now();
|
|
8125
9256
|
const issues = [];
|
|
@@ -8149,6 +9280,9 @@ async function runSingleCheck(name, projectRoot, config) {
|
|
|
8149
9280
|
case "arch":
|
|
8150
9281
|
issues.push(...await runArchCheck(projectRoot, config));
|
|
8151
9282
|
break;
|
|
9283
|
+
case "traceability":
|
|
9284
|
+
issues.push(...await runTraceabilityCheck(projectRoot, config));
|
|
9285
|
+
break;
|
|
8152
9286
|
}
|
|
8153
9287
|
} catch (error) {
|
|
8154
9288
|
issues.push({
|
|
@@ -8217,7 +9351,7 @@ async function runCIChecks(input) {
|
|
|
8217
9351
|
}
|
|
8218
9352
|
|
|
8219
9353
|
// src/review/mechanical-checks.ts
|
|
8220
|
-
import * as
|
|
9354
|
+
import * as path18 from "path";
|
|
8221
9355
|
async function runMechanicalChecks(options) {
|
|
8222
9356
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
8223
9357
|
const findings = [];
|
|
@@ -8229,7 +9363,7 @@ async function runMechanicalChecks(options) {
|
|
|
8229
9363
|
};
|
|
8230
9364
|
if (!skip.includes("validate")) {
|
|
8231
9365
|
try {
|
|
8232
|
-
const agentsPath =
|
|
9366
|
+
const agentsPath = path18.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
8233
9367
|
const result = await validateAgentsMap(agentsPath);
|
|
8234
9368
|
if (!result.ok) {
|
|
8235
9369
|
statuses.validate = "fail";
|
|
@@ -8266,7 +9400,7 @@ async function runMechanicalChecks(options) {
|
|
|
8266
9400
|
statuses.validate = "fail";
|
|
8267
9401
|
findings.push({
|
|
8268
9402
|
tool: "validate",
|
|
8269
|
-
file:
|
|
9403
|
+
file: path18.join(projectRoot, "AGENTS.md"),
|
|
8270
9404
|
message: err instanceof Error ? err.message : String(err),
|
|
8271
9405
|
severity: "error"
|
|
8272
9406
|
});
|
|
@@ -8330,7 +9464,7 @@ async function runMechanicalChecks(options) {
|
|
|
8330
9464
|
(async () => {
|
|
8331
9465
|
const localFindings = [];
|
|
8332
9466
|
try {
|
|
8333
|
-
const docsDir =
|
|
9467
|
+
const docsDir = path18.join(projectRoot, config.docsDir ?? "docs");
|
|
8334
9468
|
const result = await checkDocCoverage("project", { docsDir });
|
|
8335
9469
|
if (!result.ok) {
|
|
8336
9470
|
statuses["check-docs"] = "warn";
|
|
@@ -8357,7 +9491,7 @@ async function runMechanicalChecks(options) {
|
|
|
8357
9491
|
statuses["check-docs"] = "warn";
|
|
8358
9492
|
localFindings.push({
|
|
8359
9493
|
tool: "check-docs",
|
|
8360
|
-
file:
|
|
9494
|
+
file: path18.join(projectRoot, "docs"),
|
|
8361
9495
|
message: err instanceof Error ? err.message : String(err),
|
|
8362
9496
|
severity: "warning"
|
|
8363
9497
|
});
|
|
@@ -8505,7 +9639,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
8505
9639
|
}
|
|
8506
9640
|
|
|
8507
9641
|
// src/review/context-scoper.ts
|
|
8508
|
-
import * as
|
|
9642
|
+
import * as path19 from "path";
|
|
8509
9643
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
|
|
8510
9644
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
8511
9645
|
function computeContextBudget(diffLines) {
|
|
@@ -8513,18 +9647,18 @@ function computeContextBudget(diffLines) {
|
|
|
8513
9647
|
return diffLines;
|
|
8514
9648
|
}
|
|
8515
9649
|
function isWithinProject(absPath, projectRoot) {
|
|
8516
|
-
const resolvedRoot =
|
|
8517
|
-
const resolvedPath =
|
|
8518
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
9650
|
+
const resolvedRoot = path19.resolve(projectRoot) + path19.sep;
|
|
9651
|
+
const resolvedPath = path19.resolve(absPath);
|
|
9652
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path19.resolve(projectRoot);
|
|
8519
9653
|
}
|
|
8520
9654
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
8521
|
-
const absPath =
|
|
9655
|
+
const absPath = path19.isAbsolute(filePath) ? filePath : path19.join(projectRoot, filePath);
|
|
8522
9656
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
8523
9657
|
const result = await readFileContent(absPath);
|
|
8524
9658
|
if (!result.ok) return null;
|
|
8525
9659
|
const content = result.value;
|
|
8526
9660
|
const lines = content.split("\n").length;
|
|
8527
|
-
const relPath =
|
|
9661
|
+
const relPath = path19.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
8528
9662
|
return { path: relPath, content, reason, lines };
|
|
8529
9663
|
}
|
|
8530
9664
|
function extractImportSources(content) {
|
|
@@ -8539,18 +9673,18 @@ function extractImportSources(content) {
|
|
|
8539
9673
|
}
|
|
8540
9674
|
async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
8541
9675
|
if (!importSource.startsWith(".")) return null;
|
|
8542
|
-
const fromDir =
|
|
8543
|
-
const basePath =
|
|
9676
|
+
const fromDir = path19.dirname(path19.join(projectRoot, fromFile));
|
|
9677
|
+
const basePath = path19.resolve(fromDir, importSource);
|
|
8544
9678
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
8545
9679
|
const relBase = relativePosix(projectRoot, basePath);
|
|
8546
9680
|
const candidates = [
|
|
8547
9681
|
relBase + ".ts",
|
|
8548
9682
|
relBase + ".tsx",
|
|
8549
9683
|
relBase + ".mts",
|
|
8550
|
-
|
|
9684
|
+
path19.join(relBase, "index.ts")
|
|
8551
9685
|
];
|
|
8552
9686
|
for (const candidate of candidates) {
|
|
8553
|
-
const absCandidate =
|
|
9687
|
+
const absCandidate = path19.join(projectRoot, candidate);
|
|
8554
9688
|
if (await fileExists(absCandidate)) {
|
|
8555
9689
|
return candidate;
|
|
8556
9690
|
}
|
|
@@ -8558,7 +9692,7 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
|
8558
9692
|
return null;
|
|
8559
9693
|
}
|
|
8560
9694
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
8561
|
-
const baseName =
|
|
9695
|
+
const baseName = path19.basename(sourceFile, path19.extname(sourceFile));
|
|
8562
9696
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
8563
9697
|
const results = await findFiles(pattern, projectRoot);
|
|
8564
9698
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -9367,7 +10501,7 @@ async function fanOutReview(options) {
|
|
|
9367
10501
|
}
|
|
9368
10502
|
|
|
9369
10503
|
// src/review/validate-findings.ts
|
|
9370
|
-
import * as
|
|
10504
|
+
import * as path20 from "path";
|
|
9371
10505
|
var DOWNGRADE_MAP = {
|
|
9372
10506
|
critical: "important",
|
|
9373
10507
|
important: "suggestion",
|
|
@@ -9388,7 +10522,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
9388
10522
|
let normalized = filePath;
|
|
9389
10523
|
normalized = normalized.replace(/\\/g, "/");
|
|
9390
10524
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
9391
|
-
if (
|
|
10525
|
+
if (path20.isAbsolute(normalized)) {
|
|
9392
10526
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
9393
10527
|
if (normalized.startsWith(root)) {
|
|
9394
10528
|
normalized = normalized.slice(root.length);
|
|
@@ -9413,12 +10547,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
9413
10547
|
while ((match = importRegex.exec(content)) !== null) {
|
|
9414
10548
|
const importPath = match[1];
|
|
9415
10549
|
if (!importPath.startsWith(".")) continue;
|
|
9416
|
-
const dir =
|
|
9417
|
-
let resolved =
|
|
10550
|
+
const dir = path20.dirname(current.file);
|
|
10551
|
+
let resolved = path20.join(dir, importPath).replace(/\\/g, "/");
|
|
9418
10552
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
9419
10553
|
resolved += ".ts";
|
|
9420
10554
|
}
|
|
9421
|
-
resolved =
|
|
10555
|
+
resolved = path20.normalize(resolved).replace(/\\/g, "/");
|
|
9422
10556
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
9423
10557
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
9424
10558
|
}
|
|
@@ -9435,7 +10569,7 @@ async function validateFindings(options) {
|
|
|
9435
10569
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
9436
10570
|
continue;
|
|
9437
10571
|
}
|
|
9438
|
-
const absoluteFile =
|
|
10572
|
+
const absoluteFile = path20.isAbsolute(finding.file) ? finding.file : path20.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
9439
10573
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
9440
10574
|
continue;
|
|
9441
10575
|
}
|
|
@@ -10048,203 +11182,6 @@ async function runReviewPipeline(options) {
|
|
|
10048
11182
|
};
|
|
10049
11183
|
}
|
|
10050
11184
|
|
|
10051
|
-
// src/roadmap/parse.ts
|
|
10052
|
-
import { Ok as Ok2, Err as Err2 } from "@harness-engineering/types";
|
|
10053
|
-
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
10054
|
-
"backlog",
|
|
10055
|
-
"planned",
|
|
10056
|
-
"in-progress",
|
|
10057
|
-
"done",
|
|
10058
|
-
"blocked"
|
|
10059
|
-
]);
|
|
10060
|
-
var EM_DASH = "\u2014";
|
|
10061
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
10062
|
-
function parseRoadmap(markdown) {
|
|
10063
|
-
const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
10064
|
-
if (!fmMatch) {
|
|
10065
|
-
return Err2(new Error("Missing or malformed YAML frontmatter"));
|
|
10066
|
-
}
|
|
10067
|
-
const fmResult = parseFrontmatter2(fmMatch[1]);
|
|
10068
|
-
if (!fmResult.ok) return fmResult;
|
|
10069
|
-
const body = markdown.slice(fmMatch[0].length);
|
|
10070
|
-
const milestonesResult = parseMilestones(body);
|
|
10071
|
-
if (!milestonesResult.ok) return milestonesResult;
|
|
10072
|
-
const historyResult = parseAssignmentHistory(body);
|
|
10073
|
-
if (!historyResult.ok) return historyResult;
|
|
10074
|
-
return Ok2({
|
|
10075
|
-
frontmatter: fmResult.value,
|
|
10076
|
-
milestones: milestonesResult.value,
|
|
10077
|
-
assignmentHistory: historyResult.value
|
|
10078
|
-
});
|
|
10079
|
-
}
|
|
10080
|
-
function parseFrontmatter2(raw) {
|
|
10081
|
-
const lines = raw.split("\n");
|
|
10082
|
-
const map = /* @__PURE__ */ new Map();
|
|
10083
|
-
for (const line of lines) {
|
|
10084
|
-
const idx = line.indexOf(":");
|
|
10085
|
-
if (idx === -1) continue;
|
|
10086
|
-
const key = line.slice(0, idx).trim();
|
|
10087
|
-
const val = line.slice(idx + 1).trim();
|
|
10088
|
-
map.set(key, val);
|
|
10089
|
-
}
|
|
10090
|
-
const project = map.get("project");
|
|
10091
|
-
const versionStr = map.get("version");
|
|
10092
|
-
const lastSynced = map.get("last_synced");
|
|
10093
|
-
const lastManualEdit = map.get("last_manual_edit");
|
|
10094
|
-
const created = map.get("created");
|
|
10095
|
-
const updated = map.get("updated");
|
|
10096
|
-
if (!project || !versionStr || !lastSynced || !lastManualEdit) {
|
|
10097
|
-
return Err2(
|
|
10098
|
-
new Error(
|
|
10099
|
-
"Frontmatter missing required fields: project, version, last_synced, last_manual_edit"
|
|
10100
|
-
)
|
|
10101
|
-
);
|
|
10102
|
-
}
|
|
10103
|
-
const version = parseInt(versionStr, 10);
|
|
10104
|
-
if (isNaN(version)) {
|
|
10105
|
-
return Err2(new Error("Frontmatter version must be a number"));
|
|
10106
|
-
}
|
|
10107
|
-
const fm = { project, version, lastSynced, lastManualEdit };
|
|
10108
|
-
if (created) fm.created = created;
|
|
10109
|
-
if (updated) fm.updated = updated;
|
|
10110
|
-
return Ok2(fm);
|
|
10111
|
-
}
|
|
10112
|
-
function parseMilestones(body) {
|
|
10113
|
-
const milestones = [];
|
|
10114
|
-
const h2Pattern = /^## (.+)$/gm;
|
|
10115
|
-
const h2Matches = [];
|
|
10116
|
-
let match;
|
|
10117
|
-
let bodyEnd = body.length;
|
|
10118
|
-
while ((match = h2Pattern.exec(body)) !== null) {
|
|
10119
|
-
if (match[1] === "Assignment History") {
|
|
10120
|
-
bodyEnd = match.index;
|
|
10121
|
-
break;
|
|
10122
|
-
}
|
|
10123
|
-
h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
10124
|
-
}
|
|
10125
|
-
for (let i = 0; i < h2Matches.length; i++) {
|
|
10126
|
-
const h2 = h2Matches[i];
|
|
10127
|
-
const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
|
|
10128
|
-
const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
|
|
10129
|
-
const isBacklog = h2.heading === "Backlog";
|
|
10130
|
-
const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
|
|
10131
|
-
const featuresResult = parseFeatures(sectionBody);
|
|
10132
|
-
if (!featuresResult.ok) return featuresResult;
|
|
10133
|
-
milestones.push({
|
|
10134
|
-
name: milestoneName,
|
|
10135
|
-
isBacklog,
|
|
10136
|
-
features: featuresResult.value
|
|
10137
|
-
});
|
|
10138
|
-
}
|
|
10139
|
-
return Ok2(milestones);
|
|
10140
|
-
}
|
|
10141
|
-
function parseFeatures(sectionBody) {
|
|
10142
|
-
const features = [];
|
|
10143
|
-
const h3Pattern = /^### (?:Feature: )?(.+)$/gm;
|
|
10144
|
-
const h3Matches = [];
|
|
10145
|
-
let match;
|
|
10146
|
-
while ((match = h3Pattern.exec(sectionBody)) !== null) {
|
|
10147
|
-
h3Matches.push({ name: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
10148
|
-
}
|
|
10149
|
-
for (let i = 0; i < h3Matches.length; i++) {
|
|
10150
|
-
const h3 = h3Matches[i];
|
|
10151
|
-
const nextStart = i + 1 < h3Matches.length ? h3Matches[i + 1].startIndex : sectionBody.length;
|
|
10152
|
-
const featureBody = sectionBody.slice(h3.startIndex + h3.fullMatch.length, nextStart);
|
|
10153
|
-
const featureResult = parseFeatureFields(h3.name, featureBody);
|
|
10154
|
-
if (!featureResult.ok) return featureResult;
|
|
10155
|
-
features.push(featureResult.value);
|
|
10156
|
-
}
|
|
10157
|
-
return Ok2(features);
|
|
10158
|
-
}
|
|
10159
|
-
function extractFieldMap(body) {
|
|
10160
|
-
const fieldMap = /* @__PURE__ */ new Map();
|
|
10161
|
-
const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
|
|
10162
|
-
let match;
|
|
10163
|
-
while ((match = fieldPattern.exec(body)) !== null) {
|
|
10164
|
-
fieldMap.set(match[1], match[2]);
|
|
10165
|
-
}
|
|
10166
|
-
return fieldMap;
|
|
10167
|
-
}
|
|
10168
|
-
function parseListField(fieldMap, ...keys) {
|
|
10169
|
-
let raw = EM_DASH;
|
|
10170
|
-
for (const key of keys) {
|
|
10171
|
-
const val = fieldMap.get(key);
|
|
10172
|
-
if (val !== void 0) {
|
|
10173
|
-
raw = val;
|
|
10174
|
-
break;
|
|
10175
|
-
}
|
|
10176
|
-
}
|
|
10177
|
-
if (raw === EM_DASH || raw === "none") return [];
|
|
10178
|
-
return raw.split(",").map((s) => s.trim());
|
|
10179
|
-
}
|
|
10180
|
-
function parseFeatureFields(name, body) {
|
|
10181
|
-
const fieldMap = extractFieldMap(body);
|
|
10182
|
-
const statusRaw = fieldMap.get("Status");
|
|
10183
|
-
if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
|
|
10184
|
-
return Err2(
|
|
10185
|
-
new Error(
|
|
10186
|
-
`Feature "${name}" has invalid status: "${statusRaw ?? "(missing)"}". Valid statuses: ${[...VALID_STATUSES].join(", ")}`
|
|
10187
|
-
)
|
|
10188
|
-
);
|
|
10189
|
-
}
|
|
10190
|
-
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
10191
|
-
const plans = parseListField(fieldMap, "Plans", "Plan");
|
|
10192
|
-
const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
|
|
10193
|
-
const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
|
|
10194
|
-
const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
|
|
10195
|
-
const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
|
|
10196
|
-
if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
|
|
10197
|
-
return Err2(
|
|
10198
|
-
new Error(
|
|
10199
|
-
`Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
|
|
10200
|
-
)
|
|
10201
|
-
);
|
|
10202
|
-
}
|
|
10203
|
-
return Ok2({
|
|
10204
|
-
name,
|
|
10205
|
-
status: statusRaw,
|
|
10206
|
-
spec: specRaw === EM_DASH ? null : specRaw,
|
|
10207
|
-
plans,
|
|
10208
|
-
blockedBy,
|
|
10209
|
-
summary: fieldMap.get("Summary") ?? "",
|
|
10210
|
-
assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
|
|
10211
|
-
priority: priorityRaw === EM_DASH ? null : priorityRaw,
|
|
10212
|
-
externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
|
|
10213
|
-
});
|
|
10214
|
-
}
|
|
10215
|
-
function parseAssignmentHistory(body) {
|
|
10216
|
-
const historyMatch = body.match(/^## Assignment History\s*\n/m);
|
|
10217
|
-
if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
|
|
10218
|
-
const historyStart = historyMatch.index + historyMatch[0].length;
|
|
10219
|
-
const rawHistoryBody = body.slice(historyStart);
|
|
10220
|
-
const nextH2 = rawHistoryBody.search(/^## /m);
|
|
10221
|
-
const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
|
|
10222
|
-
const records = [];
|
|
10223
|
-
const lines = historyBody.split("\n");
|
|
10224
|
-
let pastHeader = false;
|
|
10225
|
-
for (const line of lines) {
|
|
10226
|
-
const trimmed = line.trim();
|
|
10227
|
-
if (!trimmed.startsWith("|")) continue;
|
|
10228
|
-
if (!pastHeader) {
|
|
10229
|
-
if (trimmed.match(/^\|[-\s|]+\|$/)) {
|
|
10230
|
-
pastHeader = true;
|
|
10231
|
-
}
|
|
10232
|
-
continue;
|
|
10233
|
-
}
|
|
10234
|
-
const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
10235
|
-
if (cells.length < 4) continue;
|
|
10236
|
-
const action = cells[2];
|
|
10237
|
-
if (!["assigned", "completed", "unassigned"].includes(action)) continue;
|
|
10238
|
-
records.push({
|
|
10239
|
-
feature: cells[0],
|
|
10240
|
-
assignee: cells[1],
|
|
10241
|
-
action,
|
|
10242
|
-
date: cells[3]
|
|
10243
|
-
});
|
|
10244
|
-
}
|
|
10245
|
-
return Ok2(records);
|
|
10246
|
-
}
|
|
10247
|
-
|
|
10248
11185
|
// src/roadmap/serialize.ts
|
|
10249
11186
|
var EM_DASH2 = "\u2014";
|
|
10250
11187
|
function serializeRoadmap(roadmap) {
|
|
@@ -10315,8 +11252,8 @@ function serializeAssignmentHistory(records) {
|
|
|
10315
11252
|
}
|
|
10316
11253
|
|
|
10317
11254
|
// src/roadmap/sync.ts
|
|
10318
|
-
import * as
|
|
10319
|
-
import * as
|
|
11255
|
+
import * as fs21 from "fs";
|
|
11256
|
+
import * as path21 from "path";
|
|
10320
11257
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
10321
11258
|
|
|
10322
11259
|
// src/roadmap/status-rank.ts
|
|
@@ -10346,10 +11283,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
10346
11283
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
10347
11284
|
const useRootState = featuresWithPlans.length <= 1;
|
|
10348
11285
|
if (useRootState) {
|
|
10349
|
-
const rootStatePath =
|
|
10350
|
-
if (
|
|
11286
|
+
const rootStatePath = path21.join(projectPath, ".harness", "state.json");
|
|
11287
|
+
if (fs21.existsSync(rootStatePath)) {
|
|
10351
11288
|
try {
|
|
10352
|
-
const raw =
|
|
11289
|
+
const raw = fs21.readFileSync(rootStatePath, "utf-8");
|
|
10353
11290
|
const state = JSON.parse(raw);
|
|
10354
11291
|
if (state.progress) {
|
|
10355
11292
|
for (const status of Object.values(state.progress)) {
|
|
@@ -10360,16 +11297,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
10360
11297
|
}
|
|
10361
11298
|
}
|
|
10362
11299
|
}
|
|
10363
|
-
const sessionsDir =
|
|
10364
|
-
if (
|
|
11300
|
+
const sessionsDir = path21.join(projectPath, ".harness", "sessions");
|
|
11301
|
+
if (fs21.existsSync(sessionsDir)) {
|
|
10365
11302
|
try {
|
|
10366
|
-
const sessionDirs =
|
|
11303
|
+
const sessionDirs = fs21.readdirSync(sessionsDir, { withFileTypes: true });
|
|
10367
11304
|
for (const entry of sessionDirs) {
|
|
10368
11305
|
if (!entry.isDirectory()) continue;
|
|
10369
|
-
const autopilotPath =
|
|
10370
|
-
if (!
|
|
11306
|
+
const autopilotPath = path21.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
11307
|
+
if (!fs21.existsSync(autopilotPath)) continue;
|
|
10371
11308
|
try {
|
|
10372
|
-
const raw =
|
|
11309
|
+
const raw = fs21.readFileSync(autopilotPath, "utf-8");
|
|
10373
11310
|
const autopilot = JSON.parse(raw);
|
|
10374
11311
|
if (!autopilot.phases) continue;
|
|
10375
11312
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -10466,6 +11403,13 @@ function labelsForStatus(status, config) {
|
|
|
10466
11403
|
return [...base];
|
|
10467
11404
|
}
|
|
10468
11405
|
var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
|
|
11406
|
+
function parseRepoParts(repo) {
|
|
11407
|
+
const parts = (repo ?? "").split("/");
|
|
11408
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
11409
|
+
throw new Error(`Invalid repo format: "${repo}". Expected "owner/repo".`);
|
|
11410
|
+
}
|
|
11411
|
+
return { owner: parts[0], repo: parts[1] };
|
|
11412
|
+
}
|
|
10469
11413
|
function sleep(ms) {
|
|
10470
11414
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
10471
11415
|
}
|
|
@@ -10509,12 +11453,9 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
10509
11453
|
maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
|
|
10510
11454
|
baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
|
|
10511
11455
|
};
|
|
10512
|
-
const
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
}
|
|
10516
|
-
this.owner = repoParts[0];
|
|
10517
|
-
this.repo = repoParts[1];
|
|
11456
|
+
const { owner, repo } = parseRepoParts(options.config.repo);
|
|
11457
|
+
this.owner = owner;
|
|
11458
|
+
this.repo = repo;
|
|
10518
11459
|
}
|
|
10519
11460
|
/**
|
|
10520
11461
|
* Fetch all GitHub milestones and build the name -> ID cache.
|
|
@@ -10765,7 +11706,7 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
10765
11706
|
};
|
|
10766
11707
|
|
|
10767
11708
|
// src/roadmap/sync-engine.ts
|
|
10768
|
-
import * as
|
|
11709
|
+
import * as fs22 from "fs";
|
|
10769
11710
|
function emptySyncResult() {
|
|
10770
11711
|
return { created: [], updated: [], assignmentChanges: [], errors: [] };
|
|
10771
11712
|
}
|
|
@@ -10853,7 +11794,7 @@ async function fullSync(roadmapPath, adapter, config, options) {
|
|
|
10853
11794
|
});
|
|
10854
11795
|
await previousSync;
|
|
10855
11796
|
try {
|
|
10856
|
-
const raw =
|
|
11797
|
+
const raw = fs22.readFileSync(roadmapPath, "utf-8");
|
|
10857
11798
|
const parseResult = parseRoadmap(raw);
|
|
10858
11799
|
if (!parseResult.ok) {
|
|
10859
11800
|
return {
|
|
@@ -10864,7 +11805,7 @@ async function fullSync(roadmapPath, adapter, config, options) {
|
|
|
10864
11805
|
const roadmap = parseResult.value;
|
|
10865
11806
|
const pushResult = await syncToExternal(roadmap, adapter, config);
|
|
10866
11807
|
const pullResult = await syncFromExternal(roadmap, adapter, config, options);
|
|
10867
|
-
|
|
11808
|
+
fs22.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
|
|
10868
11809
|
return {
|
|
10869
11810
|
created: pushResult.created,
|
|
10870
11811
|
updated: pushResult.updated,
|
|
@@ -10994,47 +11935,47 @@ function assignFeature(roadmap, feature, assignee, date) {
|
|
|
10994
11935
|
}
|
|
10995
11936
|
|
|
10996
11937
|
// src/interaction/types.ts
|
|
10997
|
-
import { z as
|
|
10998
|
-
var InteractionTypeSchema =
|
|
10999
|
-
var QuestionSchema =
|
|
11000
|
-
text:
|
|
11001
|
-
options:
|
|
11002
|
-
default:
|
|
11938
|
+
import { z as z9 } from "zod";
|
|
11939
|
+
var InteractionTypeSchema = z9.enum(["question", "confirmation", "transition"]);
|
|
11940
|
+
var QuestionSchema = z9.object({
|
|
11941
|
+
text: z9.string(),
|
|
11942
|
+
options: z9.array(z9.string()).optional(),
|
|
11943
|
+
default: z9.string().optional()
|
|
11003
11944
|
});
|
|
11004
|
-
var ConfirmationSchema =
|
|
11005
|
-
text:
|
|
11006
|
-
context:
|
|
11945
|
+
var ConfirmationSchema = z9.object({
|
|
11946
|
+
text: z9.string(),
|
|
11947
|
+
context: z9.string()
|
|
11007
11948
|
});
|
|
11008
|
-
var TransitionSchema =
|
|
11009
|
-
completedPhase:
|
|
11010
|
-
suggestedNext:
|
|
11011
|
-
reason:
|
|
11012
|
-
artifacts:
|
|
11013
|
-
requiresConfirmation:
|
|
11014
|
-
summary:
|
|
11949
|
+
var TransitionSchema = z9.object({
|
|
11950
|
+
completedPhase: z9.string(),
|
|
11951
|
+
suggestedNext: z9.string(),
|
|
11952
|
+
reason: z9.string(),
|
|
11953
|
+
artifacts: z9.array(z9.string()),
|
|
11954
|
+
requiresConfirmation: z9.boolean(),
|
|
11955
|
+
summary: z9.string()
|
|
11015
11956
|
});
|
|
11016
|
-
var EmitInteractionInputSchema =
|
|
11017
|
-
path:
|
|
11957
|
+
var EmitInteractionInputSchema = z9.object({
|
|
11958
|
+
path: z9.string(),
|
|
11018
11959
|
type: InteractionTypeSchema,
|
|
11019
|
-
stream:
|
|
11960
|
+
stream: z9.string().optional(),
|
|
11020
11961
|
question: QuestionSchema.optional(),
|
|
11021
11962
|
confirmation: ConfirmationSchema.optional(),
|
|
11022
11963
|
transition: TransitionSchema.optional()
|
|
11023
11964
|
});
|
|
11024
11965
|
|
|
11025
11966
|
// src/blueprint/scanner.ts
|
|
11026
|
-
import * as
|
|
11027
|
-
import * as
|
|
11967
|
+
import * as fs23 from "fs/promises";
|
|
11968
|
+
import * as path22 from "path";
|
|
11028
11969
|
var ProjectScanner = class {
|
|
11029
11970
|
constructor(rootDir) {
|
|
11030
11971
|
this.rootDir = rootDir;
|
|
11031
11972
|
}
|
|
11032
11973
|
rootDir;
|
|
11033
11974
|
async scan() {
|
|
11034
|
-
let projectName =
|
|
11975
|
+
let projectName = path22.basename(this.rootDir);
|
|
11035
11976
|
try {
|
|
11036
|
-
const pkgPath =
|
|
11037
|
-
const pkgRaw = await
|
|
11977
|
+
const pkgPath = path22.join(this.rootDir, "package.json");
|
|
11978
|
+
const pkgRaw = await fs23.readFile(pkgPath, "utf-8");
|
|
11038
11979
|
const pkg = JSON.parse(pkgRaw);
|
|
11039
11980
|
if (pkg.name) projectName = pkg.name;
|
|
11040
11981
|
} catch {
|
|
@@ -11075,8 +12016,8 @@ var ProjectScanner = class {
|
|
|
11075
12016
|
};
|
|
11076
12017
|
|
|
11077
12018
|
// src/blueprint/generator.ts
|
|
11078
|
-
import * as
|
|
11079
|
-
import * as
|
|
12019
|
+
import * as fs24 from "fs/promises";
|
|
12020
|
+
import * as path23 from "path";
|
|
11080
12021
|
import * as ejs from "ejs";
|
|
11081
12022
|
|
|
11082
12023
|
// src/blueprint/templates.ts
|
|
@@ -11160,19 +12101,19 @@ var BlueprintGenerator = class {
|
|
|
11160
12101
|
styles: STYLES,
|
|
11161
12102
|
scripts: SCRIPTS
|
|
11162
12103
|
});
|
|
11163
|
-
await
|
|
11164
|
-
await
|
|
12104
|
+
await fs24.mkdir(options.outputDir, { recursive: true });
|
|
12105
|
+
await fs24.writeFile(path23.join(options.outputDir, "index.html"), html);
|
|
11165
12106
|
}
|
|
11166
12107
|
};
|
|
11167
12108
|
|
|
11168
12109
|
// src/update-checker.ts
|
|
11169
|
-
import * as
|
|
11170
|
-
import * as
|
|
12110
|
+
import * as fs25 from "fs";
|
|
12111
|
+
import * as path24 from "path";
|
|
11171
12112
|
import * as os from "os";
|
|
11172
12113
|
import { spawn } from "child_process";
|
|
11173
12114
|
function getStatePath() {
|
|
11174
12115
|
const home = process.env["HOME"] || os.homedir();
|
|
11175
|
-
return
|
|
12116
|
+
return path24.join(home, ".harness", "update-check.json");
|
|
11176
12117
|
}
|
|
11177
12118
|
function isUpdateCheckEnabled(configInterval) {
|
|
11178
12119
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -11185,7 +12126,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
11185
12126
|
}
|
|
11186
12127
|
function readCheckState() {
|
|
11187
12128
|
try {
|
|
11188
|
-
const raw =
|
|
12129
|
+
const raw = fs25.readFileSync(getStatePath(), "utf-8");
|
|
11189
12130
|
const parsed = JSON.parse(raw);
|
|
11190
12131
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
11191
12132
|
const state = parsed;
|
|
@@ -11202,7 +12143,7 @@ function readCheckState() {
|
|
|
11202
12143
|
}
|
|
11203
12144
|
function spawnBackgroundCheck(currentVersion) {
|
|
11204
12145
|
const statePath = getStatePath();
|
|
11205
|
-
const stateDir =
|
|
12146
|
+
const stateDir = path24.dirname(statePath);
|
|
11206
12147
|
const script = `
|
|
11207
12148
|
const { execSync } = require('child_process');
|
|
11208
12149
|
const fs = require('fs');
|
|
@@ -11291,9 +12232,9 @@ async function resolveWasmPath(grammarName) {
|
|
|
11291
12232
|
const { createRequire } = await import("module");
|
|
11292
12233
|
const require2 = createRequire(import.meta.url ?? __filename);
|
|
11293
12234
|
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
11294
|
-
const
|
|
11295
|
-
const pkgDir =
|
|
11296
|
-
return
|
|
12235
|
+
const path28 = await import("path");
|
|
12236
|
+
const pkgDir = path28.dirname(pkgPath);
|
|
12237
|
+
return path28.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
11297
12238
|
}
|
|
11298
12239
|
async function loadLanguage(lang) {
|
|
11299
12240
|
const grammarName = GRAMMAR_MAP[lang];
|
|
@@ -11697,8 +12638,8 @@ function getModelPrice(model, dataset) {
|
|
|
11697
12638
|
}
|
|
11698
12639
|
|
|
11699
12640
|
// src/pricing/cache.ts
|
|
11700
|
-
import * as
|
|
11701
|
-
import * as
|
|
12641
|
+
import * as fs26 from "fs/promises";
|
|
12642
|
+
import * as path25 from "path";
|
|
11702
12643
|
|
|
11703
12644
|
// src/pricing/fallback.json
|
|
11704
12645
|
var fallback_default = {
|
|
@@ -11751,14 +12692,14 @@ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/mai
|
|
|
11751
12692
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11752
12693
|
var STALENESS_WARNING_DAYS = 7;
|
|
11753
12694
|
function getCachePath(projectRoot) {
|
|
11754
|
-
return
|
|
12695
|
+
return path25.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
11755
12696
|
}
|
|
11756
12697
|
function getStalenessMarkerPath(projectRoot) {
|
|
11757
|
-
return
|
|
12698
|
+
return path25.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
11758
12699
|
}
|
|
11759
12700
|
async function readDiskCache(projectRoot) {
|
|
11760
12701
|
try {
|
|
11761
|
-
const raw = await
|
|
12702
|
+
const raw = await fs26.readFile(getCachePath(projectRoot), "utf-8");
|
|
11762
12703
|
return JSON.parse(raw);
|
|
11763
12704
|
} catch {
|
|
11764
12705
|
return null;
|
|
@@ -11766,8 +12707,8 @@ async function readDiskCache(projectRoot) {
|
|
|
11766
12707
|
}
|
|
11767
12708
|
async function writeDiskCache(projectRoot, data) {
|
|
11768
12709
|
const cachePath = getCachePath(projectRoot);
|
|
11769
|
-
await
|
|
11770
|
-
await
|
|
12710
|
+
await fs26.mkdir(path25.dirname(cachePath), { recursive: true });
|
|
12711
|
+
await fs26.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
11771
12712
|
}
|
|
11772
12713
|
async function fetchFromNetwork() {
|
|
11773
12714
|
try {
|
|
@@ -11794,7 +12735,7 @@ function loadFallbackDataset() {
|
|
|
11794
12735
|
async function checkAndWarnStaleness(projectRoot) {
|
|
11795
12736
|
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
11796
12737
|
try {
|
|
11797
|
-
const raw = await
|
|
12738
|
+
const raw = await fs26.readFile(markerPath, "utf-8");
|
|
11798
12739
|
const marker = JSON.parse(raw);
|
|
11799
12740
|
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
11800
12741
|
const now = Date.now();
|
|
@@ -11806,8 +12747,8 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
11806
12747
|
}
|
|
11807
12748
|
} catch {
|
|
11808
12749
|
try {
|
|
11809
|
-
await
|
|
11810
|
-
await
|
|
12750
|
+
await fs26.mkdir(path25.dirname(markerPath), { recursive: true });
|
|
12751
|
+
await fs26.writeFile(
|
|
11811
12752
|
markerPath,
|
|
11812
12753
|
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
11813
12754
|
);
|
|
@@ -11817,7 +12758,7 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
11817
12758
|
}
|
|
11818
12759
|
async function clearStalenessMarker(projectRoot) {
|
|
11819
12760
|
try {
|
|
11820
|
-
await
|
|
12761
|
+
await fs26.unlink(getStalenessMarkerPath(projectRoot));
|
|
11821
12762
|
} catch {
|
|
11822
12763
|
}
|
|
11823
12764
|
}
|
|
@@ -11987,8 +12928,8 @@ function aggregateByDay(records) {
|
|
|
11987
12928
|
}
|
|
11988
12929
|
|
|
11989
12930
|
// src/usage/jsonl-reader.ts
|
|
11990
|
-
import * as
|
|
11991
|
-
import * as
|
|
12931
|
+
import * as fs27 from "fs";
|
|
12932
|
+
import * as path26 from "path";
|
|
11992
12933
|
function parseLine(line, lineNumber) {
|
|
11993
12934
|
let entry;
|
|
11994
12935
|
try {
|
|
@@ -12027,10 +12968,10 @@ function parseLine(line, lineNumber) {
|
|
|
12027
12968
|
return record;
|
|
12028
12969
|
}
|
|
12029
12970
|
function readCostRecords(projectRoot) {
|
|
12030
|
-
const costsFile =
|
|
12971
|
+
const costsFile = path26.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
12031
12972
|
let raw;
|
|
12032
12973
|
try {
|
|
12033
|
-
raw =
|
|
12974
|
+
raw = fs27.readFileSync(costsFile, "utf-8");
|
|
12034
12975
|
} catch {
|
|
12035
12976
|
return [];
|
|
12036
12977
|
}
|
|
@@ -12048,8 +12989,8 @@ function readCostRecords(projectRoot) {
|
|
|
12048
12989
|
}
|
|
12049
12990
|
|
|
12050
12991
|
// src/usage/cc-parser.ts
|
|
12051
|
-
import * as
|
|
12052
|
-
import * as
|
|
12992
|
+
import * as fs28 from "fs";
|
|
12993
|
+
import * as path27 from "path";
|
|
12053
12994
|
import * as os2 from "os";
|
|
12054
12995
|
function extractUsage(entry) {
|
|
12055
12996
|
if (entry.type !== "assistant") return null;
|
|
@@ -12082,7 +13023,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
12082
13023
|
entry = JSON.parse(line);
|
|
12083
13024
|
} catch {
|
|
12084
13025
|
console.warn(
|
|
12085
|
-
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${
|
|
13026
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path27.basename(filePath)}`
|
|
12086
13027
|
);
|
|
12087
13028
|
return null;
|
|
12088
13029
|
}
|
|
@@ -12096,7 +13037,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
12096
13037
|
function readCCFile(filePath) {
|
|
12097
13038
|
let raw;
|
|
12098
13039
|
try {
|
|
12099
|
-
raw =
|
|
13040
|
+
raw = fs28.readFileSync(filePath, "utf-8");
|
|
12100
13041
|
} catch {
|
|
12101
13042
|
return [];
|
|
12102
13043
|
}
|
|
@@ -12118,10 +13059,10 @@ function readCCFile(filePath) {
|
|
|
12118
13059
|
}
|
|
12119
13060
|
function parseCCRecords() {
|
|
12120
13061
|
const homeDir = process.env.HOME ?? os2.homedir();
|
|
12121
|
-
const projectsDir =
|
|
13062
|
+
const projectsDir = path27.join(homeDir, ".claude", "projects");
|
|
12122
13063
|
let projectDirs;
|
|
12123
13064
|
try {
|
|
12124
|
-
projectDirs =
|
|
13065
|
+
projectDirs = fs28.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path27.join(projectsDir, d.name));
|
|
12125
13066
|
} catch {
|
|
12126
13067
|
return [];
|
|
12127
13068
|
}
|
|
@@ -12129,7 +13070,7 @@ function parseCCRecords() {
|
|
|
12129
13070
|
for (const dir of projectDirs) {
|
|
12130
13071
|
let files;
|
|
12131
13072
|
try {
|
|
12132
|
-
files =
|
|
13073
|
+
files = fs28.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path27.join(dir, f));
|
|
12133
13074
|
} catch {
|
|
12134
13075
|
continue;
|
|
12135
13076
|
}
|
|
@@ -12145,6 +13086,7 @@ var VERSION = "0.15.0";
|
|
|
12145
13086
|
export {
|
|
12146
13087
|
AGENT_DESCRIPTORS,
|
|
12147
13088
|
ARCHITECTURE_DESCRIPTOR,
|
|
13089
|
+
AdjustedForecastSchema,
|
|
12148
13090
|
AgentActionEmitter,
|
|
12149
13091
|
ArchBaselineManager,
|
|
12150
13092
|
ArchBaselineSchema,
|
|
@@ -12160,23 +13102,29 @@ export {
|
|
|
12160
13102
|
CACHE_TTL_MS,
|
|
12161
13103
|
COMPLIANCE_DESCRIPTOR,
|
|
12162
13104
|
CategoryBaselineSchema,
|
|
13105
|
+
CategoryForecastSchema,
|
|
12163
13106
|
CategoryRegressionSchema,
|
|
13107
|
+
CategorySnapshotSchema,
|
|
12164
13108
|
ChecklistBuilder,
|
|
12165
13109
|
CircularDepsCollector,
|
|
12166
13110
|
ComplexityCollector,
|
|
13111
|
+
ConfidenceTierSchema,
|
|
12167
13112
|
ConfirmationSchema,
|
|
12168
13113
|
ConsoleSink,
|
|
12169
13114
|
ConstraintRuleSchema,
|
|
12170
13115
|
ContentPipeline,
|
|
13116
|
+
ContributingFeatureSchema,
|
|
12171
13117
|
ContributionsSchema,
|
|
12172
13118
|
CouplingCollector,
|
|
12173
13119
|
CriticalPathResolver,
|
|
12174
13120
|
DEFAULT_PROVIDER_TIERS,
|
|
12175
13121
|
DEFAULT_SECURITY_CONFIG,
|
|
13122
|
+
DEFAULT_STABILITY_THRESHOLDS,
|
|
12176
13123
|
DEFAULT_STATE,
|
|
12177
13124
|
DEFAULT_STREAM_INDEX,
|
|
12178
13125
|
DESTRUCTIVE_BASH,
|
|
12179
13126
|
DepDepthCollector,
|
|
13127
|
+
DirectionSchema,
|
|
12180
13128
|
EXTENSION_MAP,
|
|
12181
13129
|
EmitInteractionInputSchema,
|
|
12182
13130
|
EntropyAnalyzer,
|
|
@@ -12202,6 +13150,11 @@ export {
|
|
|
12202
13150
|
NoOpSink,
|
|
12203
13151
|
NoOpTelemetryAdapter,
|
|
12204
13152
|
PatternConfigSchema,
|
|
13153
|
+
PredictionEngine,
|
|
13154
|
+
PredictionOptionsSchema,
|
|
13155
|
+
RegressionResultSchema as PredictionRegressionResultSchema,
|
|
13156
|
+
PredictionResultSchema,
|
|
13157
|
+
PredictionWarningSchema,
|
|
12205
13158
|
ProjectScanner,
|
|
12206
13159
|
QuestionSchema,
|
|
12207
13160
|
REQUIRED_SECTIONS,
|
|
@@ -12217,10 +13170,19 @@ export {
|
|
|
12217
13170
|
SharableLayerSchema,
|
|
12218
13171
|
SharableSecurityRulesSchema,
|
|
12219
13172
|
SkillEventSchema,
|
|
13173
|
+
SpecImpactEstimateSchema,
|
|
13174
|
+
SpecImpactEstimator,
|
|
13175
|
+
SpecImpactSignalsSchema,
|
|
13176
|
+
StabilityForecastSchema,
|
|
12220
13177
|
StreamIndexSchema,
|
|
12221
13178
|
StreamInfoSchema,
|
|
12222
13179
|
ThresholdConfigSchema,
|
|
13180
|
+
TimelineFileSchema,
|
|
13181
|
+
TimelineManager,
|
|
13182
|
+
TimelineSnapshotSchema,
|
|
12223
13183
|
TransitionSchema,
|
|
13184
|
+
TrendLineSchema,
|
|
13185
|
+
TrendResultSchema,
|
|
12224
13186
|
TypeScriptParser,
|
|
12225
13187
|
VERSION,
|
|
12226
13188
|
ViolationSchema,
|
|
@@ -12235,6 +13197,7 @@ export {
|
|
|
12235
13197
|
appendSessionEntry,
|
|
12236
13198
|
applyFixes,
|
|
12237
13199
|
applyHotspotDowngrade,
|
|
13200
|
+
applyRecencyWeights,
|
|
12238
13201
|
applySyncChanges,
|
|
12239
13202
|
archMatchers,
|
|
12240
13203
|
archModule,
|
|
@@ -12252,6 +13215,7 @@ export {
|
|
|
12252
13215
|
checkEligibility,
|
|
12253
13216
|
checkEvidenceCoverage,
|
|
12254
13217
|
checkTaint,
|
|
13218
|
+
classifyConfidence,
|
|
12255
13219
|
classifyFinding,
|
|
12256
13220
|
clearEventHashCache,
|
|
12257
13221
|
clearFailuresCache,
|
|
@@ -12354,7 +13318,7 @@ export {
|
|
|
12354
13318
|
parseDateFromEntry,
|
|
12355
13319
|
parseDiff,
|
|
12356
13320
|
parseFile,
|
|
12357
|
-
parseFrontmatter,
|
|
13321
|
+
parseFrontmatter2 as parseFrontmatter,
|
|
12358
13322
|
parseHarnessIgnore,
|
|
12359
13323
|
parseLiteLLMData,
|
|
12360
13324
|
parseManifest,
|
|
@@ -12363,6 +13327,7 @@ export {
|
|
|
12363
13327
|
parseSize,
|
|
12364
13328
|
pathTraversalRules,
|
|
12365
13329
|
previewFix,
|
|
13330
|
+
projectValue,
|
|
12366
13331
|
promoteSessionLearnings,
|
|
12367
13332
|
pruneLearnings,
|
|
12368
13333
|
reactRules,
|
|
@@ -12430,6 +13395,8 @@ export {
|
|
|
12430
13395
|
validateKnowledgeMap,
|
|
12431
13396
|
validatePatternConfig,
|
|
12432
13397
|
violationId,
|
|
13398
|
+
weeksUntilThreshold,
|
|
13399
|
+
weightedLinearRegression,
|
|
12433
13400
|
writeConfig,
|
|
12434
13401
|
writeLockfile,
|
|
12435
13402
|
writeSessionSummary,
|