@arcbridge/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +37 -11
- package/dist/index.js +335 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -130,7 +130,8 @@ var TaskStatusSchema = z4.enum([
|
|
|
130
130
|
"todo",
|
|
131
131
|
"in-progress",
|
|
132
132
|
"done",
|
|
133
|
-
"blocked"
|
|
133
|
+
"blocked",
|
|
134
|
+
"cancelled"
|
|
134
135
|
]);
|
|
135
136
|
var PhaseSchema = z4.object({
|
|
136
137
|
id: z4.string().min(1),
|
|
@@ -300,7 +301,7 @@ function transaction(db, fn) {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
// src/db/schema.ts
|
|
303
|
-
var CURRENT_SCHEMA_VERSION =
|
|
304
|
+
var CURRENT_SCHEMA_VERSION = 3;
|
|
304
305
|
var SCHEMA_SQL = `
|
|
305
306
|
-- Metadata
|
|
306
307
|
CREATE TABLE IF NOT EXISTS arcbridge_meta (
|
|
@@ -447,7 +448,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
447
448
|
phase_id TEXT NOT NULL REFERENCES phases(id),
|
|
448
449
|
title TEXT NOT NULL,
|
|
449
450
|
description TEXT,
|
|
450
|
-
status TEXT NOT NULL DEFAULT 'todo' CHECK(status IN ('todo','in-progress','done','blocked')),
|
|
451
|
+
status TEXT NOT NULL DEFAULT 'todo' CHECK(status IN ('todo','in-progress','done','blocked','cancelled')),
|
|
451
452
|
building_block TEXT REFERENCES building_blocks(id),
|
|
452
453
|
quality_scenarios TEXT NOT NULL DEFAULT '[]',
|
|
453
454
|
acceptance_criteria TEXT NOT NULL DEFAULT '[]',
|
|
@@ -545,6 +546,29 @@ var migrations = [
|
|
|
545
546
|
CREATE INDEX IF NOT EXISTS idx_activity_phase ON agent_activity(phase_id);
|
|
546
547
|
`);
|
|
547
548
|
}
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
version: 3,
|
|
552
|
+
up: (db) => {
|
|
553
|
+
db.exec(`
|
|
554
|
+
CREATE TABLE tasks_new (
|
|
555
|
+
id TEXT PRIMARY KEY,
|
|
556
|
+
phase_id TEXT NOT NULL REFERENCES phases(id),
|
|
557
|
+
title TEXT NOT NULL,
|
|
558
|
+
description TEXT,
|
|
559
|
+
status TEXT NOT NULL DEFAULT 'todo' CHECK(status IN ('todo','in-progress','done','blocked','cancelled')),
|
|
560
|
+
building_block TEXT REFERENCES building_blocks(id),
|
|
561
|
+
quality_scenarios TEXT NOT NULL DEFAULT '[]',
|
|
562
|
+
acceptance_criteria TEXT NOT NULL DEFAULT '[]',
|
|
563
|
+
created_at TEXT NOT NULL,
|
|
564
|
+
completed_at TEXT
|
|
565
|
+
);
|
|
566
|
+
INSERT INTO tasks_new (id, phase_id, title, description, status, building_block, quality_scenarios, acceptance_criteria, created_at, completed_at)
|
|
567
|
+
SELECT id, phase_id, title, description, status, building_block, quality_scenarios, acceptance_criteria, created_at, completed_at FROM tasks;
|
|
568
|
+
DROP TABLE tasks;
|
|
569
|
+
ALTER TABLE tasks_new RENAME TO tasks;
|
|
570
|
+
`);
|
|
571
|
+
}
|
|
548
572
|
}
|
|
549
573
|
];
|
|
550
574
|
function migrate(db) {
|
|
@@ -738,7 +762,7 @@ function generateConfig(targetDir, input) {
|
|
|
738
762
|
|
|
739
763
|
// src/generators/arc42-generator.ts
|
|
740
764
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
741
|
-
import { join as
|
|
765
|
+
import { join as join4 } from "path";
|
|
742
766
|
import matter from "gray-matter";
|
|
743
767
|
import { stringify as stringify2 } from "yaml";
|
|
744
768
|
|
|
@@ -826,6 +850,10 @@ ${techStack(input.template)}
|
|
|
826
850
|
};
|
|
827
851
|
}
|
|
828
852
|
|
|
853
|
+
// src/templates/arc42/05-building-blocks.ts
|
|
854
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
855
|
+
import { join as join3 } from "path";
|
|
856
|
+
|
|
829
857
|
// src/templates/arc42/detect-layout.ts
|
|
830
858
|
import { existsSync } from "fs";
|
|
831
859
|
import { join as join2 } from "path";
|
|
@@ -959,13 +987,31 @@ function buildingBlocksTemplate(input) {
|
|
|
959
987
|
}
|
|
960
988
|
return blocks;
|
|
961
989
|
}
|
|
962
|
-
function buildDotnetBlocks(
|
|
990
|
+
function buildDotnetBlocks(inp) {
|
|
991
|
+
const root = inp.projectRoot ?? ".";
|
|
992
|
+
let prefix = "";
|
|
993
|
+
try {
|
|
994
|
+
const primaryService = inp.dotnetServices?.find((s) => !s.path.includes("Test"));
|
|
995
|
+
if (primaryService && primaryService.path !== ".") {
|
|
996
|
+
prefix = primaryService.path.endsWith("/") ? primaryService.path : `${primaryService.path}/`;
|
|
997
|
+
} else {
|
|
998
|
+
const srcDir = join3(root, "src");
|
|
999
|
+
if (existsSync2(srcDir)) {
|
|
1000
|
+
const entries = readdirSync(srcDir).sort();
|
|
1001
|
+
const projDir = entries.find(
|
|
1002
|
+
(e) => existsSync2(join3(srcDir, e, `${e}.csproj`)) || existsSync2(join3(srcDir, e, "Program.cs"))
|
|
1003
|
+
);
|
|
1004
|
+
if (projDir) prefix = `src/${projDir}/`;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
} catch {
|
|
1008
|
+
}
|
|
963
1009
|
const blocks = [
|
|
964
1010
|
{
|
|
965
1011
|
id: "api-host",
|
|
966
1012
|
name: "API Host",
|
|
967
1013
|
level: 1,
|
|
968
|
-
code_paths: [
|
|
1014
|
+
code_paths: [`${prefix}Program.cs`, `${prefix}Extensions/`],
|
|
969
1015
|
interfaces: [],
|
|
970
1016
|
quality_scenarios: [],
|
|
971
1017
|
adrs: [],
|
|
@@ -976,7 +1022,7 @@ function buildingBlocksTemplate(input) {
|
|
|
976
1022
|
id: "controllers",
|
|
977
1023
|
name: "Controllers / Endpoints",
|
|
978
1024
|
level: 1,
|
|
979
|
-
code_paths: [
|
|
1025
|
+
code_paths: [`${prefix}Controllers/`, `${prefix}Endpoints/`],
|
|
980
1026
|
interfaces: [],
|
|
981
1027
|
quality_scenarios: ["SEC-03"],
|
|
982
1028
|
adrs: [],
|
|
@@ -987,7 +1033,7 @@ function buildingBlocksTemplate(input) {
|
|
|
987
1033
|
id: "domain",
|
|
988
1034
|
name: "Domain",
|
|
989
1035
|
level: 1,
|
|
990
|
-
code_paths: [
|
|
1036
|
+
code_paths: [`${prefix}Domain/`, `${prefix}Models/`],
|
|
991
1037
|
interfaces: [],
|
|
992
1038
|
quality_scenarios: [],
|
|
993
1039
|
adrs: [],
|
|
@@ -998,7 +1044,7 @@ function buildingBlocksTemplate(input) {
|
|
|
998
1044
|
id: "services",
|
|
999
1045
|
name: "Application Services",
|
|
1000
1046
|
level: 1,
|
|
1001
|
-
code_paths: [
|
|
1047
|
+
code_paths: [`${prefix}Services/`],
|
|
1002
1048
|
interfaces: ["controllers"],
|
|
1003
1049
|
quality_scenarios: [],
|
|
1004
1050
|
adrs: [],
|
|
@@ -1009,7 +1055,7 @@ function buildingBlocksTemplate(input) {
|
|
|
1009
1055
|
id: "middleware",
|
|
1010
1056
|
name: "Middleware",
|
|
1011
1057
|
level: 1,
|
|
1012
|
-
code_paths: [
|
|
1058
|
+
code_paths: [`${prefix}Middleware/`],
|
|
1013
1059
|
interfaces: [],
|
|
1014
1060
|
quality_scenarios: ["REL-01"],
|
|
1015
1061
|
adrs: [],
|
|
@@ -1021,7 +1067,7 @@ function buildingBlocksTemplate(input) {
|
|
|
1021
1067
|
id: "auth-module",
|
|
1022
1068
|
name: "Authentication & Authorization",
|
|
1023
1069
|
level: 1,
|
|
1024
|
-
code_paths: [
|
|
1070
|
+
code_paths: [`${prefix}Auth/`],
|
|
1025
1071
|
interfaces: [],
|
|
1026
1072
|
quality_scenarios: ["SEC-01", "SEC-02"],
|
|
1027
1073
|
adrs: [],
|
|
@@ -1032,7 +1078,7 @@ function buildingBlocksTemplate(input) {
|
|
|
1032
1078
|
id: "data-access",
|
|
1033
1079
|
name: "Data Access",
|
|
1034
1080
|
level: 1,
|
|
1035
|
-
code_paths: [
|
|
1081
|
+
code_paths: [`${prefix}Data/`, `${prefix}Repositories/`, `${prefix}Migrations/`],
|
|
1036
1082
|
interfaces: ["domain"],
|
|
1037
1083
|
quality_scenarios: [],
|
|
1038
1084
|
adrs: [],
|
|
@@ -1847,8 +1893,8 @@ function writeMarkdownWithFrontmatter(filePath, frontmatter, body) {
|
|
|
1847
1893
|
writeFileSync2(filePath, content, "utf-8");
|
|
1848
1894
|
}
|
|
1849
1895
|
function generateArc42(targetDir, input) {
|
|
1850
|
-
const arc42Dir =
|
|
1851
|
-
const decisionsDir =
|
|
1896
|
+
const arc42Dir = join4(targetDir, ".arcbridge", "arc42");
|
|
1897
|
+
const decisionsDir = join4(arc42Dir, "09-decisions");
|
|
1852
1898
|
mkdirSync2(arc42Dir, { recursive: true });
|
|
1853
1899
|
mkdirSync2(decisionsDir, { recursive: true });
|
|
1854
1900
|
const sections = [
|
|
@@ -1863,17 +1909,17 @@ function generateArc42(targetDir, input) {
|
|
|
1863
1909
|
const inputWithRoot = { ...input, projectRoot: targetDir };
|
|
1864
1910
|
for (const { file, template } of sections) {
|
|
1865
1911
|
const { frontmatter, body } = template(inputWithRoot);
|
|
1866
|
-
writeMarkdownWithFrontmatter(
|
|
1912
|
+
writeMarkdownWithFrontmatter(join4(arc42Dir, file), frontmatter, body);
|
|
1867
1913
|
}
|
|
1868
1914
|
const adr = firstAdrTemplate(inputWithRoot);
|
|
1869
1915
|
writeMarkdownWithFrontmatter(
|
|
1870
|
-
|
|
1916
|
+
join4(decisionsDir, adr.filename),
|
|
1871
1917
|
adr.frontmatter,
|
|
1872
1918
|
adr.body
|
|
1873
1919
|
);
|
|
1874
1920
|
const qualityScenarios = qualityScenariosTemplate(input);
|
|
1875
1921
|
writeFileSync2(
|
|
1876
|
-
|
|
1922
|
+
join4(arc42Dir, "10-quality-scenarios.yaml"),
|
|
1877
1923
|
stringify2(qualityScenarios),
|
|
1878
1924
|
"utf-8"
|
|
1879
1925
|
);
|
|
@@ -1881,7 +1927,7 @@ function generateArc42(targetDir, input) {
|
|
|
1881
1927
|
|
|
1882
1928
|
// src/generators/plan-generator.ts
|
|
1883
1929
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1884
|
-
import { join as
|
|
1930
|
+
import { join as join5 } from "path";
|
|
1885
1931
|
import { stringify as stringify3 } from "yaml";
|
|
1886
1932
|
|
|
1887
1933
|
// src/templates/phases/nextjs-app-router.ts
|
|
@@ -2805,25 +2851,25 @@ var planTemplates = {
|
|
|
2805
2851
|
"dotnet-webapi": { plan: phasePlanTemplate4, tasks: phaseTasksTemplate4 }
|
|
2806
2852
|
};
|
|
2807
2853
|
function generatePlan(targetDir, input) {
|
|
2808
|
-
const planDir =
|
|
2809
|
-
const tasksDir =
|
|
2854
|
+
const planDir = join5(targetDir, ".arcbridge", "plan");
|
|
2855
|
+
const tasksDir = join5(planDir, "tasks");
|
|
2810
2856
|
mkdirSync3(planDir, { recursive: true });
|
|
2811
2857
|
mkdirSync3(tasksDir, { recursive: true });
|
|
2812
2858
|
const tmpl = planTemplates[input.template] ?? planTemplates["nextjs-app-router"];
|
|
2813
2859
|
const phasePlan = tmpl.plan(input);
|
|
2814
|
-
writeFileSync3(
|
|
2860
|
+
writeFileSync3(join5(planDir, "phases.yaml"), stringify3(phasePlan), "utf-8");
|
|
2815
2861
|
for (const phase of phasePlan.phases) {
|
|
2816
2862
|
const taskFile = tmpl.tasks(input, phase.id);
|
|
2817
2863
|
if (taskFile) {
|
|
2818
2864
|
writeFileSync3(
|
|
2819
|
-
|
|
2865
|
+
join5(tasksDir, `${phase.id}.yaml`),
|
|
2820
2866
|
stringify3(taskFile),
|
|
2821
2867
|
"utf-8"
|
|
2822
2868
|
);
|
|
2823
2869
|
}
|
|
2824
2870
|
}
|
|
2825
2871
|
writeFileSync3(
|
|
2826
|
-
|
|
2872
|
+
join5(planDir, "sync-log.md"),
|
|
2827
2873
|
`# Sync Log
|
|
2828
2874
|
|
|
2829
2875
|
Architecture sync events are recorded here.
|
|
@@ -2834,7 +2880,7 @@ Architecture sync events are recorded here.
|
|
|
2834
2880
|
|
|
2835
2881
|
// src/generators/agent-generator.ts
|
|
2836
2882
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2837
|
-
import { join as
|
|
2883
|
+
import { join as join6 } from "path";
|
|
2838
2884
|
import matter2 from "gray-matter";
|
|
2839
2885
|
|
|
2840
2886
|
// src/templates/agents/architect.ts
|
|
@@ -2930,7 +2976,9 @@ You are responsible for maintaining these sections in \`.arcbridge/arc42/\`. Upd
|
|
|
2930
2976
|
|
|
2931
2977
|
## Project Planning
|
|
2932
2978
|
|
|
2933
|
-
At the start of a project (after init), plan
|
|
2979
|
+
At the start of a project (after init), plan the full roadmap:
|
|
2980
|
+
- **ArcBridge generates 4 phases as a starting template** (Setup, Foundation, Features, Polish). These are a baseline \u2014 adapt them to your project's actual scope and complexity.
|
|
2981
|
+
- **For larger projects, add more phases** using \`arcbridge_create_phase\`. Split "Core Features" into multiple phases if needed (e.g., "Auth & Users", "Core Business Logic", "Integrations", "Polish").
|
|
2934
2982
|
- **Phase 0-1 tasks are ready to use** \u2014 they cover setup and foundation for this template
|
|
2935
2983
|
- **Phase 2+ tasks are examples** \u2014 replace them with real tasks derived from the project's requirements and specifications
|
|
2936
2984
|
- Review the phase plan with \`arcbridge_get_phase_plan\` and replace example tasks with project-specific ones
|
|
@@ -2938,7 +2986,6 @@ At the start of a project (after init), plan tasks across ALL phases:
|
|
|
2938
2986
|
- Keep each phase reasonably scoped \u2014 3-6 tasks per phase is ideal
|
|
2939
2987
|
- Map tasks to building blocks so drift detection tracks coverage
|
|
2940
2988
|
- Link tasks to quality scenarios so gate checks are meaningful
|
|
2941
|
-
- Plan the full roadmap before diving into implementation \u2014 this prevents phases from becoming too large or unfocused
|
|
2942
2989
|
|
|
2943
2990
|
## Working Style
|
|
2944
2991
|
|
|
@@ -3089,11 +3136,13 @@ function qualityGuardianTemplate() {
|
|
|
3089
3136
|
"arcbridge_search_symbols",
|
|
3090
3137
|
"arcbridge_get_symbol",
|
|
3091
3138
|
"arcbridge_get_component_graph",
|
|
3092
|
-
"arcbridge_get_boundary_analysis"
|
|
3093
|
-
|
|
3139
|
+
"arcbridge_get_boundary_analysis",
|
|
3140
|
+
"arcbridge_update_scenario_status",
|
|
3141
|
+
"arcbridge_verify_scenarios",
|
|
3142
|
+
"arcbridge_run_role_check"
|
|
3094
3143
|
],
|
|
3095
3144
|
denied_tools: [],
|
|
3096
|
-
read_only:
|
|
3145
|
+
read_only: false,
|
|
3097
3146
|
quality_focus: [
|
|
3098
3147
|
"performance",
|
|
3099
3148
|
"accessibility",
|
|
@@ -3124,6 +3173,23 @@ function qualityGuardianTemplate() {
|
|
|
3124
3173
|
- Every quality scenario with status "untested" needs attention
|
|
3125
3174
|
- Performance budgets are hard limits, not guidelines
|
|
3126
3175
|
|
|
3176
|
+
## Linking Tests to Quality Scenarios
|
|
3177
|
+
|
|
3178
|
+
Quality scenarios need \`linked_tests\` to be verifiable via \`arcbridge_verify_scenarios\`. To link tests:
|
|
3179
|
+
|
|
3180
|
+
1. Edit \`.arcbridge/arc42/10-quality-scenarios.yaml\`
|
|
3181
|
+
2. Add test file paths to the \`linked_tests\` array for each scenario:
|
|
3182
|
+
\`\`\`yaml
|
|
3183
|
+
- id: SEC-01
|
|
3184
|
+
linked_tests:
|
|
3185
|
+
- "src/__tests__/auth.test.ts"
|
|
3186
|
+
- "src/__tests__/middleware.test.ts"
|
|
3187
|
+
\`\`\`
|
|
3188
|
+
3. Use glob patterns for broader matching: \`"src/**/*.security.test.ts"\`
|
|
3189
|
+
4. After linking, run \`arcbridge_verify_scenarios\` to execute the tests and update scenario status
|
|
3190
|
+
|
|
3191
|
+
Without \`linked_tests\`, scenarios remain "untested" \u2014 so link tests early, ideally when creating the test file.
|
|
3192
|
+
|
|
3127
3193
|
## Review Checklist
|
|
3128
3194
|
|
|
3129
3195
|
1. **Performance:** Bundle size, LCP, API latency against defined budgets
|
|
@@ -3145,7 +3211,12 @@ function phaseManagerTemplate() {
|
|
|
3145
3211
|
"arcbridge_get_phase_plan",
|
|
3146
3212
|
"arcbridge_get_current_tasks",
|
|
3147
3213
|
"arcbridge_update_task",
|
|
3214
|
+
"arcbridge_delete_task",
|
|
3215
|
+
"arcbridge_create_task",
|
|
3216
|
+
"arcbridge_create_phase",
|
|
3148
3217
|
"arcbridge_check_drift",
|
|
3218
|
+
"arcbridge_verify_scenarios",
|
|
3219
|
+
"arcbridge_update_scenario_status",
|
|
3149
3220
|
"arcbridge_get_open_questions",
|
|
3150
3221
|
"arcbridge_propose_arc42_update"
|
|
3151
3222
|
],
|
|
@@ -3180,9 +3251,11 @@ function phaseManagerTemplate() {
|
|
|
3180
3251
|
## Task Planning
|
|
3181
3252
|
|
|
3182
3253
|
Before starting any phase, ensure proper task planning:
|
|
3254
|
+
- **ArcBridge generates 4 phases as a starting template.** For larger projects, add more phases using \`arcbridge_create_phase\`.
|
|
3183
3255
|
- **Phase 0-1 tasks are concrete** \u2014 they cover project setup and foundation. Follow them as-is.
|
|
3184
3256
|
- **Phase 2+ tasks are examples only** \u2014 they show the *shape* of later phases but must be replaced with real tasks derived from the project's actual requirements and specs.
|
|
3185
|
-
- **At project start, plan ALL phases** \u2014 review \`arcbridge_get_phase_plan\`, delete example tasks in Phase 2
|
|
3257
|
+
- **At project start, plan ALL phases** \u2014 review \`arcbridge_get_phase_plan\`, delete example tasks in Phase 2+ using \`arcbridge_delete_task\`, and create real tasks based on the product spec.
|
|
3258
|
+
- **Delete vs cancel** \u2014 use \`arcbridge_delete_task\` for example/template tasks that should be removed. Use \`arcbridge_update_task\` with status \`cancelled\` for planned tasks that turned out to be unnecessary (preserves the decision trail).
|
|
3186
3259
|
- **Keep phases small and focused** \u2014 if a phase has more than 6-8 tasks, split it into sub-phases
|
|
3187
3260
|
- **Tasks should be concrete and verifiable** \u2014 each task needs clear acceptance criteria
|
|
3188
3261
|
- **Link tasks to building blocks** \u2014 this enables drift detection and progress tracking
|
|
@@ -3194,8 +3267,9 @@ Before starting any phase, ensure proper task planning:
|
|
|
3194
3267
|
2. Review task coverage for the NEXT phase \u2014 create tasks if empty
|
|
3195
3268
|
3. Run drift detection (\`arcbridge_check_drift\`)
|
|
3196
3269
|
4. Propose arc42 updates if drift is detected
|
|
3197
|
-
5.
|
|
3198
|
-
6.
|
|
3270
|
+
5. Ensure quality scenarios have \`linked_tests\` \u2014 edit \`10-quality-scenarios.yaml\` to add test file paths so \`arcbridge_verify_scenarios\` can run them
|
|
3271
|
+
6. Check quality gate requirements
|
|
3272
|
+
7. Mark phase complete or report blockers`
|
|
3199
3273
|
};
|
|
3200
3274
|
}
|
|
3201
3275
|
|
|
@@ -3422,10 +3496,10 @@ var UI_TEMPLATES = /* @__PURE__ */ new Set(["nextjs-app-router", "react-vite"]);
|
|
|
3422
3496
|
function writeAgentRole(dir, role) {
|
|
3423
3497
|
const { system_prompt, ...frontmatter } = role;
|
|
3424
3498
|
const content = matter2.stringify(system_prompt, frontmatter);
|
|
3425
|
-
writeFileSync4(
|
|
3499
|
+
writeFileSync4(join6(dir, `${role.role_id}.md`), content, "utf-8");
|
|
3426
3500
|
}
|
|
3427
3501
|
function generateAgentRoles(targetDir, template) {
|
|
3428
|
-
const agentsDir =
|
|
3502
|
+
const agentsDir = join6(targetDir, ".arcbridge", "agents");
|
|
3429
3503
|
mkdirSync4(agentsDir, { recursive: true });
|
|
3430
3504
|
const roles = [
|
|
3431
3505
|
architectTemplate(),
|
|
@@ -3446,19 +3520,19 @@ function generateAgentRoles(targetDir, template) {
|
|
|
3446
3520
|
}
|
|
3447
3521
|
|
|
3448
3522
|
// src/generators/db-generator.ts
|
|
3449
|
-
import { join as
|
|
3450
|
-
import { existsSync as
|
|
3523
|
+
import { join as join7 } from "path";
|
|
3524
|
+
import { existsSync as existsSync3, readFileSync, readdirSync as readdirSync2, appendFileSync } from "fs";
|
|
3451
3525
|
import { parse } from "yaml";
|
|
3452
3526
|
import matter3 from "gray-matter";
|
|
3453
3527
|
function populateBuildingBlocks(db, targetDir) {
|
|
3454
3528
|
const warnings = [];
|
|
3455
|
-
const filePath =
|
|
3529
|
+
const filePath = join7(
|
|
3456
3530
|
targetDir,
|
|
3457
3531
|
".arcbridge",
|
|
3458
3532
|
"arc42",
|
|
3459
3533
|
"05-building-blocks.md"
|
|
3460
3534
|
);
|
|
3461
|
-
if (!
|
|
3535
|
+
if (!existsSync3(filePath)) {
|
|
3462
3536
|
warnings.push("Building blocks file not found, skipping");
|
|
3463
3537
|
return warnings;
|
|
3464
3538
|
}
|
|
@@ -3467,7 +3541,7 @@ function populateBuildingBlocks(db, targetDir) {
|
|
|
3467
3541
|
const result = BuildingBlocksFrontmatterSchema.safeParse(data);
|
|
3468
3542
|
if (!result.success) {
|
|
3469
3543
|
warnings.push(
|
|
3470
|
-
`Invalid building blocks frontmatter: ${result.error.issues.map((i) => i.message).join(", ")}`
|
|
3544
|
+
`Invalid building blocks frontmatter: ${result.error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message).join(", ")}`
|
|
3471
3545
|
);
|
|
3472
3546
|
return warnings;
|
|
3473
3547
|
}
|
|
@@ -3494,13 +3568,13 @@ function populateBuildingBlocks(db, targetDir) {
|
|
|
3494
3568
|
}
|
|
3495
3569
|
function populateQualityScenarios(db, targetDir) {
|
|
3496
3570
|
const warnings = [];
|
|
3497
|
-
const filePath =
|
|
3571
|
+
const filePath = join7(
|
|
3498
3572
|
targetDir,
|
|
3499
3573
|
".arcbridge",
|
|
3500
3574
|
"arc42",
|
|
3501
3575
|
"10-quality-scenarios.yaml"
|
|
3502
3576
|
);
|
|
3503
|
-
if (!
|
|
3577
|
+
if (!existsSync3(filePath)) {
|
|
3504
3578
|
warnings.push("Quality scenarios file not found, skipping");
|
|
3505
3579
|
return warnings;
|
|
3506
3580
|
}
|
|
@@ -3509,7 +3583,7 @@ function populateQualityScenarios(db, targetDir) {
|
|
|
3509
3583
|
const result = QualityScenariosFileSchema.safeParse(parsed);
|
|
3510
3584
|
if (!result.success) {
|
|
3511
3585
|
warnings.push(
|
|
3512
|
-
`Invalid quality scenarios: ${result.error.issues.map((i) => i.message).join(", ")}`
|
|
3586
|
+
`Invalid quality scenarios: ${result.error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message).join(", ")}`
|
|
3513
3587
|
);
|
|
3514
3588
|
return warnings;
|
|
3515
3589
|
}
|
|
@@ -3536,8 +3610,8 @@ function populateQualityScenarios(db, targetDir) {
|
|
|
3536
3610
|
}
|
|
3537
3611
|
function populatePhases(db, targetDir) {
|
|
3538
3612
|
const warnings = [];
|
|
3539
|
-
const phasesPath =
|
|
3540
|
-
if (!
|
|
3613
|
+
const phasesPath = join7(targetDir, ".arcbridge", "plan", "phases.yaml");
|
|
3614
|
+
if (!existsSync3(phasesPath)) {
|
|
3541
3615
|
warnings.push("Phases file not found, skipping");
|
|
3542
3616
|
return warnings;
|
|
3543
3617
|
}
|
|
@@ -3546,7 +3620,7 @@ function populatePhases(db, targetDir) {
|
|
|
3546
3620
|
const result = PhasesFileSchema.safeParse(parsed);
|
|
3547
3621
|
if (!result.success) {
|
|
3548
3622
|
warnings.push(
|
|
3549
|
-
`Invalid phases file: ${result.error.issues.map((i) => i.message).join(", ")}`
|
|
3623
|
+
`Invalid phases file: ${result.error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message).join(", ")}`
|
|
3550
3624
|
);
|
|
3551
3625
|
return warnings;
|
|
3552
3626
|
}
|
|
@@ -3570,20 +3644,20 @@ function populatePhases(db, targetDir) {
|
|
|
3570
3644
|
phase.started_at ?? null,
|
|
3571
3645
|
phase.completed_at ?? null
|
|
3572
3646
|
);
|
|
3573
|
-
const taskPath =
|
|
3647
|
+
const taskPath = join7(
|
|
3574
3648
|
targetDir,
|
|
3575
3649
|
".arcbridge",
|
|
3576
3650
|
"plan",
|
|
3577
3651
|
"tasks",
|
|
3578
3652
|
`${phase.id}.yaml`
|
|
3579
3653
|
);
|
|
3580
|
-
if (!
|
|
3654
|
+
if (!existsSync3(taskPath)) continue;
|
|
3581
3655
|
const taskRaw = readFileSync(taskPath, "utf-8");
|
|
3582
3656
|
const taskParsed = parse(taskRaw);
|
|
3583
3657
|
const taskResult = TaskFileSchema.safeParse(taskParsed);
|
|
3584
3658
|
if (!taskResult.success) {
|
|
3585
3659
|
warnings.push(
|
|
3586
|
-
`Invalid task file for ${phase.id}: ${taskResult.error.issues.map((i) => i.message).join(", ")}`
|
|
3660
|
+
`Invalid task file for ${phase.id}: ${taskResult.error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message).join(", ")}`
|
|
3587
3661
|
);
|
|
3588
3662
|
continue;
|
|
3589
3663
|
}
|
|
@@ -3605,27 +3679,27 @@ function populatePhases(db, targetDir) {
|
|
|
3605
3679
|
}
|
|
3606
3680
|
function populateAdrs(db, targetDir) {
|
|
3607
3681
|
const warnings = [];
|
|
3608
|
-
const decisionsDir =
|
|
3682
|
+
const decisionsDir = join7(
|
|
3609
3683
|
targetDir,
|
|
3610
3684
|
".arcbridge",
|
|
3611
3685
|
"arc42",
|
|
3612
3686
|
"09-decisions"
|
|
3613
3687
|
);
|
|
3614
|
-
if (!
|
|
3688
|
+
if (!existsSync3(decisionsDir)) {
|
|
3615
3689
|
return warnings;
|
|
3616
3690
|
}
|
|
3617
|
-
const files =
|
|
3691
|
+
const files = readdirSync2(decisionsDir).filter((f) => f.endsWith(".md"));
|
|
3618
3692
|
const insert = db.prepare(`
|
|
3619
3693
|
INSERT OR IGNORE INTO adrs (id, title, status, date, context, decision, consequences, affected_blocks, affected_files, quality_scenarios)
|
|
3620
3694
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3621
3695
|
`);
|
|
3622
3696
|
for (const file of files) {
|
|
3623
|
-
const raw = readFileSync(
|
|
3697
|
+
const raw = readFileSync(join7(decisionsDir, file), "utf-8");
|
|
3624
3698
|
const { data, content } = matter3(raw);
|
|
3625
3699
|
const result = AdrFrontmatterSchema.safeParse(data);
|
|
3626
3700
|
if (!result.success) {
|
|
3627
3701
|
warnings.push(
|
|
3628
|
-
`Invalid ADR ${file}: ${result.error.issues.map((i) => i.message).join(", ")}`
|
|
3702
|
+
`Invalid ADR ${file}: ${result.error.issues.map((i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message).join(", ")}`
|
|
3629
3703
|
);
|
|
3630
3704
|
continue;
|
|
3631
3705
|
}
|
|
@@ -3715,7 +3789,7 @@ function refreshFromDocs(db, targetDir) {
|
|
|
3715
3789
|
return warnings;
|
|
3716
3790
|
}
|
|
3717
3791
|
function generateDatabase(targetDir, input) {
|
|
3718
|
-
const dbPath =
|
|
3792
|
+
const dbPath = join7(targetDir, ".arcbridge", "index.db");
|
|
3719
3793
|
const db = openDatabase(dbPath);
|
|
3720
3794
|
initializeSchema(db);
|
|
3721
3795
|
const allWarnings = [];
|
|
@@ -3735,9 +3809,9 @@ function generateDatabase(targetDir, input) {
|
|
|
3735
3809
|
return { db, warnings: allWarnings };
|
|
3736
3810
|
}
|
|
3737
3811
|
function ensureGitignore(targetDir) {
|
|
3738
|
-
const gitignorePath =
|
|
3812
|
+
const gitignorePath = join7(targetDir, ".gitignore");
|
|
3739
3813
|
const marker = ".arcbridge/index.db";
|
|
3740
|
-
if (
|
|
3814
|
+
if (existsSync3(gitignorePath)) {
|
|
3741
3815
|
const content = readFileSync(gitignorePath, "utf-8");
|
|
3742
3816
|
if (content.includes(marker)) return;
|
|
3743
3817
|
}
|
|
@@ -3751,14 +3825,14 @@ function ensureGitignore(targetDir) {
|
|
|
3751
3825
|
}
|
|
3752
3826
|
|
|
3753
3827
|
// src/indexer/index.ts
|
|
3754
|
-
import { relative as relative5, join as
|
|
3755
|
-
import { existsSync as
|
|
3828
|
+
import { relative as relative5, join as join14 } from "path";
|
|
3829
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
3756
3830
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3757
3831
|
import YAML from "yaml";
|
|
3758
3832
|
|
|
3759
3833
|
// src/indexer/program.ts
|
|
3760
3834
|
import ts from "typescript";
|
|
3761
|
-
import { join as
|
|
3835
|
+
import { join as join8, dirname } from "path";
|
|
3762
3836
|
function createTsProgram(options) {
|
|
3763
3837
|
const projectRoot = options.projectRoot;
|
|
3764
3838
|
const configPath = options.tsconfigPath ?? ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
|
|
@@ -3782,8 +3856,8 @@ function createTsProgram(options) {
|
|
|
3782
3856
|
for (const ref of config.references) {
|
|
3783
3857
|
const refRelPath = typeof ref === "string" ? ref : ref.path;
|
|
3784
3858
|
if (!refRelPath) continue;
|
|
3785
|
-
const refFullPath =
|
|
3786
|
-
const refConfigPath = refFullPath.endsWith(".json") ? refFullPath :
|
|
3859
|
+
const refFullPath = join8(dirname(configPath), refRelPath);
|
|
3860
|
+
const refConfigPath = refFullPath.endsWith(".json") ? refFullPath : join8(refFullPath, "tsconfig.json");
|
|
3787
3861
|
if (ts.sys.fileExists(refConfigPath)) {
|
|
3788
3862
|
const refConfig = ts.readConfigFile(refConfigPath, ts.sys.readFile);
|
|
3789
3863
|
const rc = refConfig.config;
|
|
@@ -3811,7 +3885,7 @@ function createTsProgram(options) {
|
|
|
3811
3885
|
const parsed = ts.parseJsonConfigFileContent(
|
|
3812
3886
|
configFile.config,
|
|
3813
3887
|
ts.sys,
|
|
3814
|
-
|
|
3888
|
+
join8(projectRoot),
|
|
3815
3889
|
{ noEmit: true },
|
|
3816
3890
|
resolvedConfigPath
|
|
3817
3891
|
);
|
|
@@ -3821,7 +3895,7 @@ function createTsProgram(options) {
|
|
|
3821
3895
|
});
|
|
3822
3896
|
const checker = program.getTypeChecker();
|
|
3823
3897
|
const sourceFiles = program.getSourceFiles().filter(
|
|
3824
|
-
(sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules")
|
|
3898
|
+
(sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules") && !sf.fileName.includes(".next/") && !sf.fileName.includes(".next\\")
|
|
3825
3899
|
);
|
|
3826
3900
|
return { program, checker, sourceFiles, projectRoot };
|
|
3827
3901
|
}
|
|
@@ -4579,8 +4653,8 @@ function writeComponents(db, components) {
|
|
|
4579
4653
|
}
|
|
4580
4654
|
|
|
4581
4655
|
// src/indexer/route-analyzer.ts
|
|
4582
|
-
import { existsSync as
|
|
4583
|
-
import { join as
|
|
4656
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync2, statSync } from "fs";
|
|
4657
|
+
import { join as join9, relative as relative3 } from "path";
|
|
4584
4658
|
var FILE_KIND_MAP = {
|
|
4585
4659
|
"page": "page",
|
|
4586
4660
|
"layout": "layout",
|
|
@@ -4591,15 +4665,22 @@ var FILE_KIND_MAP = {
|
|
|
4591
4665
|
};
|
|
4592
4666
|
var TS_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
4593
4667
|
function analyzeRoutes(projectRoot, db, service = "main") {
|
|
4594
|
-
|
|
4595
|
-
|
|
4668
|
+
let appDir = join9(projectRoot, "app");
|
|
4669
|
+
try {
|
|
4670
|
+
if (!existsSync4(appDir) || !statSync(appDir).isDirectory()) {
|
|
4671
|
+
appDir = join9(projectRoot, "src", "app");
|
|
4672
|
+
if (!existsSync4(appDir) || !statSync(appDir).isDirectory()) {
|
|
4673
|
+
return 0;
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
} catch {
|
|
4596
4677
|
return 0;
|
|
4597
4678
|
}
|
|
4598
4679
|
const routes = [];
|
|
4599
4680
|
const layoutStack = [];
|
|
4600
4681
|
for (const ext of TS_EXTENSIONS) {
|
|
4601
|
-
const middlewarePath =
|
|
4602
|
-
if (
|
|
4682
|
+
const middlewarePath = join9(projectRoot, `middleware${ext}`);
|
|
4683
|
+
if (existsSync4(middlewarePath)) {
|
|
4603
4684
|
routes.push({
|
|
4604
4685
|
id: `route::middleware`,
|
|
4605
4686
|
routePath: "/",
|
|
@@ -4619,15 +4700,15 @@ function analyzeRoutes(projectRoot, db, service = "main") {
|
|
|
4619
4700
|
function walkAppDir(dir, routePath, routes, layoutStack, service, projectRoot) {
|
|
4620
4701
|
let entries;
|
|
4621
4702
|
try {
|
|
4622
|
-
entries =
|
|
4703
|
+
entries = readdirSync3(dir);
|
|
4623
4704
|
} catch {
|
|
4624
4705
|
return;
|
|
4625
4706
|
}
|
|
4626
4707
|
const currentLayout = layoutStack.length > 0 ? layoutStack[layoutStack.length - 1] : null;
|
|
4627
4708
|
for (const [convention, kind] of Object.entries(FILE_KIND_MAP)) {
|
|
4628
4709
|
for (const ext of TS_EXTENSIONS) {
|
|
4629
|
-
const filePath =
|
|
4630
|
-
if (
|
|
4710
|
+
const filePath = join9(dir, `${convention}${ext}`);
|
|
4711
|
+
if (existsSync4(filePath)) {
|
|
4631
4712
|
const relPath = relative3(projectRoot, dir);
|
|
4632
4713
|
const routeId = `route::${relPath}/${convention}`;
|
|
4633
4714
|
const route = {
|
|
@@ -4651,7 +4732,7 @@ function walkAppDir(dir, routePath, routes, layoutStack, service, projectRoot) {
|
|
|
4651
4732
|
}
|
|
4652
4733
|
}
|
|
4653
4734
|
for (const entry of entries.sort()) {
|
|
4654
|
-
const fullPath =
|
|
4735
|
+
const fullPath = join9(dir, entry);
|
|
4655
4736
|
try {
|
|
4656
4737
|
if (!statSync(fullPath).isDirectory()) continue;
|
|
4657
4738
|
} catch {
|
|
@@ -4817,16 +4898,16 @@ function writeDependencies(db, dependencies) {
|
|
|
4817
4898
|
|
|
4818
4899
|
// src/indexer/dotnet-indexer.ts
|
|
4819
4900
|
import { execFileSync } from "child_process";
|
|
4820
|
-
import { resolve, join as
|
|
4821
|
-
import { readdirSync as
|
|
4901
|
+
import { resolve, join as join10, dirname as dirname2, relative as relative4, basename } from "path";
|
|
4902
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync3, existsSync as existsSync5, accessSync, constants } from "fs";
|
|
4822
4903
|
import { fileURLToPath } from "url";
|
|
4823
4904
|
function findDotnetProject(projectRoot) {
|
|
4824
4905
|
try {
|
|
4825
|
-
const entries =
|
|
4906
|
+
const entries = readdirSync4(projectRoot);
|
|
4826
4907
|
const sln = entries.find((e) => e.endsWith(".sln"));
|
|
4827
|
-
if (sln) return
|
|
4908
|
+
if (sln) return join10(projectRoot, sln);
|
|
4828
4909
|
const csproj = entries.find((e) => e.endsWith(".csproj"));
|
|
4829
|
-
if (csproj) return
|
|
4910
|
+
if (csproj) return join10(projectRoot, csproj);
|
|
4830
4911
|
return null;
|
|
4831
4912
|
} catch {
|
|
4832
4913
|
return null;
|
|
@@ -4841,8 +4922,8 @@ function parseSolutionProjects(slnPath) {
|
|
|
4841
4922
|
while ((match = projectPattern.exec(content)) !== null) {
|
|
4842
4923
|
const name = match[1];
|
|
4843
4924
|
const relativeCsprojPath = match[2].replace(/\\/g, "/");
|
|
4844
|
-
const fullCsprojPath = resolve(
|
|
4845
|
-
if (!
|
|
4925
|
+
const fullCsprojPath = resolve(join10(slnDir, relativeCsprojPath));
|
|
4926
|
+
if (!existsSync5(fullCsprojPath)) continue;
|
|
4846
4927
|
const projectDir = relative4(slnDir, dirname2(fullCsprojPath)).replace(/\\/g, "/") || ".";
|
|
4847
4928
|
const isTestProject = /[.\x2d]tests?$/i.test(name) || /[.\x2d](unit|integration|functional|e2e)tests?$/i.test(name);
|
|
4848
4929
|
projects.push({
|
|
@@ -4878,7 +4959,7 @@ function hasGlobalTool() {
|
|
|
4878
4959
|
if (!dir) continue;
|
|
4879
4960
|
for (const ext of extensions) {
|
|
4880
4961
|
try {
|
|
4881
|
-
accessSync(
|
|
4962
|
+
accessSync(join10(dir, `${name}${ext}`), constants.X_OK);
|
|
4882
4963
|
return true;
|
|
4883
4964
|
} catch {
|
|
4884
4965
|
continue;
|
|
@@ -4895,7 +4976,7 @@ function resolveIndexerProject() {
|
|
|
4895
4976
|
resolve(currentDir2, "../../dotnet-indexer/ArcBridge.DotnetIndexer.csproj")
|
|
4896
4977
|
];
|
|
4897
4978
|
for (const candidate of candidates) {
|
|
4898
|
-
if (
|
|
4979
|
+
if (existsSync5(candidate)) return candidate;
|
|
4899
4980
|
}
|
|
4900
4981
|
return null;
|
|
4901
4982
|
}
|
|
@@ -5037,7 +5118,7 @@ function indexDotnetProjectRoslyn(db, options) {
|
|
|
5037
5118
|
|
|
5038
5119
|
// src/indexer/csharp/indexer.ts
|
|
5039
5120
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5040
|
-
import { join as
|
|
5121
|
+
import { join as join11 } from "path";
|
|
5041
5122
|
import { globbySync } from "globby";
|
|
5042
5123
|
|
|
5043
5124
|
// src/indexer/csharp/parser.ts
|
|
@@ -5889,7 +5970,7 @@ async function indexCSharpTreeSitter(db, options) {
|
|
|
5889
5970
|
for (const filePath of csFiles) {
|
|
5890
5971
|
const relPath = filePath.replace(/\\/g, "/");
|
|
5891
5972
|
currentPaths.add(relPath);
|
|
5892
|
-
const fullPath =
|
|
5973
|
+
const fullPath = join11(projectRoot, relPath);
|
|
5893
5974
|
const content = readFileSync4(fullPath, "utf-8");
|
|
5894
5975
|
const hash = hashContent(content);
|
|
5895
5976
|
const tree = parseCSharp(content);
|
|
@@ -5965,12 +6046,12 @@ async function indexCSharpTreeSitter(db, options) {
|
|
|
5965
6046
|
}
|
|
5966
6047
|
|
|
5967
6048
|
// src/indexer/package-deps.ts
|
|
5968
|
-
import { join as
|
|
5969
|
-
import { existsSync as
|
|
6049
|
+
import { join as join12 } from "path";
|
|
6050
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5 } from "fs";
|
|
5970
6051
|
function indexPackageDependencies(db, projectRoot, service = "main") {
|
|
5971
6052
|
const deps = [];
|
|
5972
|
-
const pkgJsonPath =
|
|
5973
|
-
if (
|
|
6053
|
+
const pkgJsonPath = join12(projectRoot, "package.json");
|
|
6054
|
+
if (existsSync6(pkgJsonPath)) {
|
|
5974
6055
|
deps.push(...parsePackageJson(pkgJsonPath));
|
|
5975
6056
|
}
|
|
5976
6057
|
const csprojFiles = findCsprojFiles(projectRoot);
|
|
@@ -6032,10 +6113,10 @@ function findCsprojFiles(dir, maxDepth = 4) {
|
|
|
6032
6113
|
function walk(currentDir2, depth) {
|
|
6033
6114
|
if (depth > maxDepth) return;
|
|
6034
6115
|
try {
|
|
6035
|
-
const entries =
|
|
6116
|
+
const entries = readdirSync5(currentDir2, { withFileTypes: true });
|
|
6036
6117
|
for (const entry of entries) {
|
|
6037
6118
|
if (entry.name === "bin" || entry.name === "obj" || entry.name === "node_modules" || entry.name === ".git") continue;
|
|
6038
|
-
const fullPath =
|
|
6119
|
+
const fullPath = join12(currentDir2, entry.name);
|
|
6039
6120
|
if (entry.isFile() && entry.name.endsWith(".csproj")) {
|
|
6040
6121
|
results.push(fullPath);
|
|
6041
6122
|
} else if (entry.isDirectory()) {
|
|
@@ -6051,10 +6132,10 @@ function findCsprojFiles(dir, maxDepth = 4) {
|
|
|
6051
6132
|
|
|
6052
6133
|
// src/config/loader.ts
|
|
6053
6134
|
import { readFileSync as readFileSync6 } from "fs";
|
|
6054
|
-
import { join as
|
|
6135
|
+
import { join as join13 } from "path";
|
|
6055
6136
|
import yaml from "yaml";
|
|
6056
6137
|
function loadConfig(projectRoot) {
|
|
6057
|
-
const configPath =
|
|
6138
|
+
const configPath = join13(projectRoot, ".arcbridge", "config.yaml");
|
|
6058
6139
|
try {
|
|
6059
6140
|
const raw = readFileSync6(configPath, "utf-8");
|
|
6060
6141
|
const parsed = ArcBridgeConfigSchema.safeParse(yaml.parse(raw));
|
|
@@ -6073,8 +6154,8 @@ function loadConfig(projectRoot) {
|
|
|
6073
6154
|
|
|
6074
6155
|
// src/indexer/index.ts
|
|
6075
6156
|
function detectProjectLanguage(projectRoot) {
|
|
6076
|
-
if (
|
|
6077
|
-
if (
|
|
6157
|
+
if (existsSync7(join14(projectRoot, "tsconfig.json"))) return "typescript";
|
|
6158
|
+
if (existsSync7(join14(projectRoot, "package.json"))) return "typescript";
|
|
6078
6159
|
if (findDotnetProject(projectRoot)) return "csharp";
|
|
6079
6160
|
return "typescript";
|
|
6080
6161
|
}
|
|
@@ -6102,7 +6183,7 @@ function resolveCSharpBackend(projectRoot) {
|
|
|
6102
6183
|
let setting = config?.indexing?.csharp_indexer;
|
|
6103
6184
|
if (!setting && error) {
|
|
6104
6185
|
try {
|
|
6105
|
-
const raw = readFileSync7(
|
|
6186
|
+
const raw = readFileSync7(join14(projectRoot, ".arcbridge", "config.yaml"), "utf-8");
|
|
6106
6187
|
const parsed = YAML.parse(raw);
|
|
6107
6188
|
const rawSetting = parsed?.indexing?.csharp_indexer;
|
|
6108
6189
|
if (rawSetting === "roslyn" || rawSetting === "tree-sitter") {
|
|
@@ -6478,18 +6559,18 @@ function safeParseJson(value, fallback) {
|
|
|
6478
6559
|
}
|
|
6479
6560
|
|
|
6480
6561
|
// src/sync/yaml-writer.ts
|
|
6481
|
-
import { join as
|
|
6482
|
-
import { existsSync as
|
|
6562
|
+
import { join as join15 } from "path";
|
|
6563
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
6483
6564
|
import { parse as parse2, stringify as stringify4 } from "yaml";
|
|
6484
6565
|
function syncTaskToYaml(projectRoot, phaseId, taskId, status, completedAt) {
|
|
6485
|
-
const taskPath =
|
|
6566
|
+
const taskPath = join15(
|
|
6486
6567
|
projectRoot,
|
|
6487
6568
|
".arcbridge",
|
|
6488
6569
|
"plan",
|
|
6489
6570
|
"tasks",
|
|
6490
6571
|
`${phaseId}.yaml`
|
|
6491
6572
|
);
|
|
6492
|
-
if (!
|
|
6573
|
+
if (!existsSync8(taskPath)) return;
|
|
6493
6574
|
const raw = readFileSync8(taskPath, "utf-8");
|
|
6494
6575
|
const parsed = parse2(raw);
|
|
6495
6576
|
const result = TaskFileSchema.safeParse(parsed);
|
|
@@ -6506,11 +6587,11 @@ function syncTaskToYaml(projectRoot, phaseId, taskId, status, completedAt) {
|
|
|
6506
6587
|
writeFileSync5(taskPath, stringify4(taskFile), "utf-8");
|
|
6507
6588
|
}
|
|
6508
6589
|
function addTaskToYaml(projectRoot, phaseId, task) {
|
|
6509
|
-
const tasksDir =
|
|
6510
|
-
const taskPath =
|
|
6590
|
+
const tasksDir = join15(projectRoot, ".arcbridge", "plan", "tasks");
|
|
6591
|
+
const taskPath = join15(tasksDir, `${phaseId}.yaml`);
|
|
6511
6592
|
mkdirSync5(tasksDir, { recursive: true });
|
|
6512
6593
|
let taskFile;
|
|
6513
|
-
if (
|
|
6594
|
+
if (existsSync8(taskPath)) {
|
|
6514
6595
|
const raw = readFileSync8(taskPath, "utf-8");
|
|
6515
6596
|
const parsed = parse2(raw);
|
|
6516
6597
|
const result = TaskFileSchema.safeParse(parsed);
|
|
@@ -6528,13 +6609,13 @@ function addTaskToYaml(projectRoot, phaseId, task) {
|
|
|
6528
6609
|
writeFileSync5(taskPath, stringify4(taskFile), "utf-8");
|
|
6529
6610
|
}
|
|
6530
6611
|
function syncPhaseToYaml(projectRoot, phaseId, status, startedAt, completedAt) {
|
|
6531
|
-
const phasesPath =
|
|
6612
|
+
const phasesPath = join15(
|
|
6532
6613
|
projectRoot,
|
|
6533
6614
|
".arcbridge",
|
|
6534
6615
|
"plan",
|
|
6535
6616
|
"phases.yaml"
|
|
6536
6617
|
);
|
|
6537
|
-
if (!
|
|
6618
|
+
if (!existsSync8(phasesPath)) return;
|
|
6538
6619
|
const raw = readFileSync8(phasesPath, "utf-8");
|
|
6539
6620
|
const parsed = parse2(raw);
|
|
6540
6621
|
const result = PhasesFileSchema.safeParse(parsed);
|
|
@@ -6547,14 +6628,71 @@ function syncPhaseToYaml(projectRoot, phaseId, status, startedAt, completedAt) {
|
|
|
6547
6628
|
if (completedAt) phase.completed_at = completedAt;
|
|
6548
6629
|
writeFileSync5(phasesPath, stringify4(phasesFile), "utf-8");
|
|
6549
6630
|
}
|
|
6550
|
-
function
|
|
6551
|
-
|
|
6631
|
+
function addPhaseToYaml(projectRoot, phase) {
|
|
6632
|
+
try {
|
|
6633
|
+
const phasesPath = join15(
|
|
6634
|
+
projectRoot,
|
|
6635
|
+
".arcbridge",
|
|
6636
|
+
"plan",
|
|
6637
|
+
"phases.yaml"
|
|
6638
|
+
);
|
|
6639
|
+
if (!existsSync8(phasesPath)) {
|
|
6640
|
+
return { success: false, warning: "phases.yaml not found" };
|
|
6641
|
+
}
|
|
6642
|
+
const raw = readFileSync8(phasesPath, "utf-8");
|
|
6643
|
+
const result = PhasesFileSchema.safeParse(parse2(raw));
|
|
6644
|
+
if (!result.success) {
|
|
6645
|
+
return { success: false, warning: "phases.yaml failed validation" };
|
|
6646
|
+
}
|
|
6647
|
+
const phasesFile = result.data;
|
|
6648
|
+
const existingById = phasesFile.phases.some((p) => p.id === phase.id);
|
|
6649
|
+
const existingByNumber = phasesFile.phases.some((p) => p.phase_number === phase.phase_number);
|
|
6650
|
+
if (existingByNumber && !existingById) {
|
|
6651
|
+
const conflicting = phasesFile.phases.find((p) => p.phase_number === phase.phase_number);
|
|
6652
|
+
return {
|
|
6653
|
+
success: false,
|
|
6654
|
+
warning: `Phase number ${phase.phase_number} already used by '${conflicting?.id}'`
|
|
6655
|
+
};
|
|
6656
|
+
}
|
|
6657
|
+
const tasksDir = join15(projectRoot, ".arcbridge", "plan", "tasks");
|
|
6658
|
+
mkdirSync5(tasksDir, { recursive: true });
|
|
6659
|
+
const taskFilePath = join15(tasksDir, `${phase.id}.yaml`);
|
|
6660
|
+
if (!existsSync8(taskFilePath)) {
|
|
6661
|
+
writeFileSync5(
|
|
6662
|
+
taskFilePath,
|
|
6663
|
+
stringify4({ schema_version: 1, phase_id: phase.id, tasks: [] }),
|
|
6664
|
+
"utf-8"
|
|
6665
|
+
);
|
|
6666
|
+
}
|
|
6667
|
+
if (existingById) {
|
|
6668
|
+
return { success: true };
|
|
6669
|
+
}
|
|
6670
|
+
phasesFile.phases.push({
|
|
6671
|
+
id: phase.id,
|
|
6672
|
+
name: phase.name,
|
|
6673
|
+
phase_number: phase.phase_number,
|
|
6674
|
+
status: "planned",
|
|
6675
|
+
description: phase.description,
|
|
6676
|
+
gate_requirements: phase.gate_requirements ?? []
|
|
6677
|
+
});
|
|
6678
|
+
phasesFile.phases.sort((a, b) => a.phase_number - b.phase_number);
|
|
6679
|
+
writeFileSync5(phasesPath, stringify4(phasesFile), "utf-8");
|
|
6680
|
+
return { success: true };
|
|
6681
|
+
} catch (err) {
|
|
6682
|
+
return {
|
|
6683
|
+
success: false,
|
|
6684
|
+
warning: `YAML write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
6685
|
+
};
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
function syncScenarioToYaml(projectRoot, scenarioId, status, linkedTests, verification) {
|
|
6689
|
+
const scenarioPath = join15(
|
|
6552
6690
|
projectRoot,
|
|
6553
6691
|
".arcbridge",
|
|
6554
6692
|
"arc42",
|
|
6555
6693
|
"10-quality-scenarios.yaml"
|
|
6556
6694
|
);
|
|
6557
|
-
if (!
|
|
6695
|
+
if (!existsSync8(scenarioPath)) return;
|
|
6558
6696
|
const raw = readFileSync8(scenarioPath, "utf-8");
|
|
6559
6697
|
const parsed = parse2(raw);
|
|
6560
6698
|
const result = QualityScenariosFileSchema.safeParse(parsed);
|
|
@@ -6563,14 +6701,55 @@ function syncScenarioToYaml(projectRoot, scenarioId, status) {
|
|
|
6563
6701
|
const scenario = scenariosFile.scenarios.find((s) => s.id === scenarioId);
|
|
6564
6702
|
if (!scenario) return;
|
|
6565
6703
|
scenario.status = status;
|
|
6704
|
+
if (linkedTests) {
|
|
6705
|
+
scenario.linked_tests = linkedTests;
|
|
6706
|
+
}
|
|
6707
|
+
if (verification) {
|
|
6708
|
+
scenario.verification = verification;
|
|
6709
|
+
}
|
|
6566
6710
|
writeFileSync5(scenarioPath, stringify4(scenariosFile), "utf-8");
|
|
6567
6711
|
}
|
|
6712
|
+
function deleteTaskFromYaml(projectRoot, phaseId, taskId) {
|
|
6713
|
+
try {
|
|
6714
|
+
const taskPath = join15(
|
|
6715
|
+
projectRoot,
|
|
6716
|
+
".arcbridge",
|
|
6717
|
+
"plan",
|
|
6718
|
+
"tasks",
|
|
6719
|
+
`${phaseId}.yaml`
|
|
6720
|
+
);
|
|
6721
|
+
if (!existsSync8(taskPath)) {
|
|
6722
|
+
return { success: true };
|
|
6723
|
+
}
|
|
6724
|
+
const raw = readFileSync8(taskPath, "utf-8");
|
|
6725
|
+
const result = TaskFileSchema.safeParse(parse2(raw));
|
|
6726
|
+
if (!result.success) {
|
|
6727
|
+
return {
|
|
6728
|
+
success: false,
|
|
6729
|
+
warning: `Could not parse ${phaseId}.yaml \u2014 task may reappear after reindex`
|
|
6730
|
+
};
|
|
6731
|
+
}
|
|
6732
|
+
const taskFile = result.data;
|
|
6733
|
+
const before = taskFile.tasks.length;
|
|
6734
|
+
taskFile.tasks = taskFile.tasks.filter((t) => t.id !== taskId);
|
|
6735
|
+
if (taskFile.tasks.length === before) {
|
|
6736
|
+
return { success: true };
|
|
6737
|
+
}
|
|
6738
|
+
writeFileSync5(taskPath, stringify4(taskFile), "utf-8");
|
|
6739
|
+
return { success: true };
|
|
6740
|
+
} catch (err) {
|
|
6741
|
+
return {
|
|
6742
|
+
success: false,
|
|
6743
|
+
warning: `YAML update failed: ${err instanceof Error ? err.message : String(err)} \u2014 task may reappear after reindex`
|
|
6744
|
+
};
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6568
6747
|
|
|
6569
6748
|
// src/sync/task-inference.ts
|
|
6570
6749
|
function inferTaskStatuses(db, phaseId) {
|
|
6571
6750
|
const results = [];
|
|
6572
6751
|
const tasks = db.prepare(
|
|
6573
|
-
"SELECT id, title, status, building_block, quality_scenarios, acceptance_criteria FROM tasks WHERE phase_id = ? AND status
|
|
6752
|
+
"SELECT id, title, status, building_block, quality_scenarios, acceptance_criteria FROM tasks WHERE phase_id = ? AND status NOT IN ('done', 'cancelled')"
|
|
6574
6753
|
).all(phaseId);
|
|
6575
6754
|
for (const task of tasks) {
|
|
6576
6755
|
const inference = inferSingleTask(db, task);
|
|
@@ -6695,7 +6874,7 @@ function safeParseJson2(value, fallback) {
|
|
|
6695
6874
|
|
|
6696
6875
|
// src/generators/sync-generator.ts
|
|
6697
6876
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
6698
|
-
import { join as
|
|
6877
|
+
import { join as join16, dirname as dirname4 } from "path";
|
|
6699
6878
|
|
|
6700
6879
|
// src/templates/sync/claude-skill.ts
|
|
6701
6880
|
function claudeSkillTemplate(config) {
|
|
@@ -6873,20 +7052,20 @@ To enable automatic sync:
|
|
|
6873
7052
|
function generateSyncFiles(targetDir, config) {
|
|
6874
7053
|
const generated = [];
|
|
6875
7054
|
const action = githubActionTemplate(config);
|
|
6876
|
-
const actionPath =
|
|
7055
|
+
const actionPath = join16(targetDir, action.relativePath);
|
|
6877
7056
|
mkdirSync6(dirname4(actionPath), { recursive: true });
|
|
6878
7057
|
writeFileSync6(actionPath, action.content, "utf-8");
|
|
6879
7058
|
generated.push(action.relativePath);
|
|
6880
7059
|
if (config.platforms.includes("claude")) {
|
|
6881
7060
|
const skill = claudeSkillTemplate(config);
|
|
6882
|
-
const skillPath =
|
|
7061
|
+
const skillPath = join16(targetDir, skill.relativePath);
|
|
6883
7062
|
mkdirSync6(dirname4(skillPath), { recursive: true });
|
|
6884
7063
|
writeFileSync6(skillPath, skill.content, "utf-8");
|
|
6885
7064
|
generated.push(skill.relativePath);
|
|
6886
7065
|
}
|
|
6887
7066
|
if (config.platforms.includes("copilot")) {
|
|
6888
7067
|
const hook = copilotHookTemplate(config);
|
|
6889
|
-
const hookPath =
|
|
7068
|
+
const hookPath = join16(targetDir, hook.relativePath);
|
|
6890
7069
|
mkdirSync6(dirname4(hookPath), { recursive: true });
|
|
6891
7070
|
writeFileSync6(hookPath, hook.content, "utf-8");
|
|
6892
7071
|
generated.push(hook.relativePath);
|
|
@@ -6896,7 +7075,7 @@ function generateSyncFiles(targetDir, config) {
|
|
|
6896
7075
|
|
|
6897
7076
|
// src/metrics/activity.ts
|
|
6898
7077
|
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
6899
|
-
import { join as
|
|
7078
|
+
import { join as join17 } from "path";
|
|
6900
7079
|
function insertActivity(db, params) {
|
|
6901
7080
|
const totalTokens = params.totalTokens ?? (params.inputTokens != null && params.outputTokens != null ? params.inputTokens + params.outputTokens : null);
|
|
6902
7081
|
const stmt = db.prepare(`
|
|
@@ -7054,7 +7233,7 @@ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
|
|
|
7054
7233
|
});
|
|
7055
7234
|
const rows = result.rows;
|
|
7056
7235
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
7057
|
-
const dir =
|
|
7236
|
+
const dir = join17(projectRoot, ".arcbridge", "metrics");
|
|
7058
7237
|
mkdirSync7(dir, { recursive: true });
|
|
7059
7238
|
let content;
|
|
7060
7239
|
let filename;
|
|
@@ -7151,7 +7330,7 @@ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
|
|
|
7151
7330
|
break;
|
|
7152
7331
|
}
|
|
7153
7332
|
}
|
|
7154
|
-
const filePath =
|
|
7333
|
+
const filePath = join17(dir, filename);
|
|
7155
7334
|
writeFileSync7(filePath, content, "utf-8");
|
|
7156
7335
|
return filePath;
|
|
7157
7336
|
}
|
|
@@ -7219,6 +7398,8 @@ function esc(val) {
|
|
|
7219
7398
|
|
|
7220
7399
|
// src/git/helpers.ts
|
|
7221
7400
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7401
|
+
import { realpathSync } from "fs";
|
|
7402
|
+
import { relative as relative6 } from "path";
|
|
7222
7403
|
function resolveRef(projectRoot, since, db) {
|
|
7223
7404
|
switch (since) {
|
|
7224
7405
|
case "last-commit":
|
|
@@ -7310,6 +7491,33 @@ function setSyncCommit(db, key, sha) {
|
|
|
7310
7491
|
"INSERT OR REPLACE INTO arcbridge_meta (key, value) VALUES (?, ?)"
|
|
7311
7492
|
).run(key, sha);
|
|
7312
7493
|
}
|
|
7494
|
+
function getRepoRoot(projectRoot) {
|
|
7495
|
+
try {
|
|
7496
|
+
return execFileSync3(
|
|
7497
|
+
"git",
|
|
7498
|
+
["rev-parse", "--show-toplevel"],
|
|
7499
|
+
{ cwd: projectRoot, encoding: "utf-8", timeout: 5e3 }
|
|
7500
|
+
).trim();
|
|
7501
|
+
} catch {
|
|
7502
|
+
return null;
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
function scopeToProject(changedFiles, projectRoot) {
|
|
7506
|
+
const repoRoot = getRepoRoot(projectRoot);
|
|
7507
|
+
if (!repoRoot) return changedFiles;
|
|
7508
|
+
let projectRel;
|
|
7509
|
+
try {
|
|
7510
|
+
projectRel = relative6(realpathSync(repoRoot), realpathSync(projectRoot));
|
|
7511
|
+
} catch {
|
|
7512
|
+
return changedFiles;
|
|
7513
|
+
}
|
|
7514
|
+
if (!projectRel || projectRel === "." || projectRel.startsWith("..")) return changedFiles;
|
|
7515
|
+
const prefix = projectRel.replace(/\\/g, "/");
|
|
7516
|
+
return changedFiles.filter((f) => f.path.startsWith(prefix + "/") || f.path === prefix).map((f) => ({
|
|
7517
|
+
...f,
|
|
7518
|
+
path: f.path.startsWith(prefix + "/") ? f.path.slice(prefix.length + 1) : f.path
|
|
7519
|
+
}));
|
|
7520
|
+
}
|
|
7313
7521
|
function parseStatusCode(code) {
|
|
7314
7522
|
switch (code.charAt(0)) {
|
|
7315
7523
|
case "A":
|
|
@@ -7325,7 +7533,7 @@ function parseStatusCode(code) {
|
|
|
7325
7533
|
|
|
7326
7534
|
// src/testing/runner.ts
|
|
7327
7535
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
7328
|
-
import { existsSync as
|
|
7536
|
+
import { existsSync as existsSync9 } from "fs";
|
|
7329
7537
|
import { resolve as resolve3 } from "path";
|
|
7330
7538
|
function verifyScenarios(db, projectRoot, options) {
|
|
7331
7539
|
const results = [];
|
|
@@ -7363,7 +7571,7 @@ function verifyScenarios(db, projectRoot, options) {
|
|
|
7363
7571
|
}
|
|
7364
7572
|
if (testPaths.length === 0) continue;
|
|
7365
7573
|
const missingPaths = testPaths.filter(
|
|
7366
|
-
(tp) => !
|
|
7574
|
+
(tp) => !existsSync9(resolve3(projectRoot, tp))
|
|
7367
7575
|
);
|
|
7368
7576
|
if (missingPaths.length === testPaths.length) {
|
|
7369
7577
|
results.push({
|
|
@@ -7378,7 +7586,7 @@ function verifyScenarios(db, projectRoot, options) {
|
|
|
7378
7586
|
continue;
|
|
7379
7587
|
}
|
|
7380
7588
|
const existingPaths = testPaths.filter(
|
|
7381
|
-
(tp) =>
|
|
7589
|
+
(tp) => existsSync9(resolve3(projectRoot, tp))
|
|
7382
7590
|
);
|
|
7383
7591
|
const start = Date.now();
|
|
7384
7592
|
let passed;
|
|
@@ -7432,21 +7640,21 @@ ${output}`;
|
|
|
7432
7640
|
}
|
|
7433
7641
|
|
|
7434
7642
|
// src/roles/loader.ts
|
|
7435
|
-
import { readdirSync as
|
|
7436
|
-
import { join as
|
|
7643
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
|
|
7644
|
+
import { join as join18 } from "path";
|
|
7437
7645
|
import matter4 from "gray-matter";
|
|
7438
7646
|
function loadRoles(projectRoot) {
|
|
7439
|
-
const agentsDir =
|
|
7647
|
+
const agentsDir = join18(projectRoot, ".arcbridge", "agents");
|
|
7440
7648
|
const roles = [];
|
|
7441
7649
|
const errors = [];
|
|
7442
7650
|
let files;
|
|
7443
7651
|
try {
|
|
7444
|
-
files =
|
|
7652
|
+
files = readdirSync6(agentsDir).filter((f) => f.endsWith(".md")).sort();
|
|
7445
7653
|
} catch {
|
|
7446
7654
|
return { roles: [], errors: [`Agent directory not found: ${agentsDir}`] };
|
|
7447
7655
|
}
|
|
7448
7656
|
for (const file of files) {
|
|
7449
|
-
const filePath =
|
|
7657
|
+
const filePath = join18(agentsDir, file);
|
|
7450
7658
|
try {
|
|
7451
7659
|
const raw = readFileSync9(filePath, "utf-8");
|
|
7452
7660
|
const parsed = matter4(raw);
|
|
@@ -7473,7 +7681,7 @@ function loadRole(projectRoot, roleId) {
|
|
|
7473
7681
|
if (!/^[a-z0-9-]+$/.test(roleId)) {
|
|
7474
7682
|
return { role: null, error: `Invalid role ID: "${roleId}" (must be kebab-case)` };
|
|
7475
7683
|
}
|
|
7476
|
-
const filePath =
|
|
7684
|
+
const filePath = join18(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
|
|
7477
7685
|
try {
|
|
7478
7686
|
const raw = readFileSync9(filePath, "utf-8");
|
|
7479
7687
|
const parsed = matter4(raw);
|
|
@@ -7510,8 +7718,10 @@ export {
|
|
|
7510
7718
|
QualityScenariosFileSchema,
|
|
7511
7719
|
TaskFileSchema,
|
|
7512
7720
|
TaskSchema,
|
|
7721
|
+
addPhaseToYaml,
|
|
7513
7722
|
addTaskToYaml,
|
|
7514
7723
|
applyInferences,
|
|
7724
|
+
deleteTaskFromYaml,
|
|
7515
7725
|
detectDrift,
|
|
7516
7726
|
detectProjectLanguage,
|
|
7517
7727
|
discoverDotnetServices,
|
|
@@ -7540,6 +7750,7 @@ export {
|
|
|
7540
7750
|
queryMetrics,
|
|
7541
7751
|
refreshFromDocs,
|
|
7542
7752
|
resolveRef,
|
|
7753
|
+
scopeToProject,
|
|
7543
7754
|
setSyncCommit,
|
|
7544
7755
|
suppressSqliteWarning,
|
|
7545
7756
|
syncPhaseToYaml,
|