@webpieces/rules-config 0.4.391 → 0.4.393

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.
@@ -1 +1 @@
1
- {"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { provideSingleton } from './di';\nimport { injectable } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n }\n}\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@provideSingleton` so it's drawn in the design. */\n@provideSingleton()\n@injectable()\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Copy-paste schema both commands print (write it / fix it).\n reviewJsonSchemaHint(filePath: string): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}`\n );\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n );\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath);\n}\n"]}
1
+ {"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n }\n}\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Copy-paste schema both commands print (write it / fix it).\n reviewJsonSchemaHint(filePath: string): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}`\n );\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n );\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath);\n}\n"]}
@@ -10,7 +10,7 @@ import { MainSyncStatusService } from './main-sync-status';
10
10
  *
11
11
  * `@DocumentDesign` marks the top of the DAG the DI-design analyzer roots on, so the library's design
12
12
  * (design.json / design.md / design.html) is generated. rules-config is the shared foundation whose
13
- * utilities are being migrated from free functions to injected `@provideSingleton` service classes; as
13
+ * utilities are being migrated from free functions to injected `@injectable(bindingScopeValues.Singleton)` service classes; as
14
14
  * each service class lands (config loader, template writer, diff/git services, …) it is injected HERE
15
15
  * so it appears in the drawn design.
16
16
  */
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RulesConfigDesign = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const di_1 = require("./di");
6
- const di_2 = require("./di");
7
6
  const inversify_1 = require("inversify");
8
7
  const repo_root_1 = require("./repo-root");
9
8
  const load_config_1 = require("./load-config");
@@ -17,7 +16,7 @@ const main_sync_status_1 = require("./main-sync-status");
17
16
  *
18
17
  * `@DocumentDesign` marks the top of the DAG the DI-design analyzer roots on, so the library's design
19
18
  * (design.json / design.md / design.html) is generated. rules-config is the shared foundation whose
20
- * utilities are being migrated from free functions to injected `@provideSingleton` service classes; as
19
+ * utilities are being migrated from free functions to injected `@injectable(bindingScopeValues.Singleton)` service classes; as
21
20
  * each service class lands (config loader, template writer, diff/git services, …) it is injected HERE
22
21
  * so it appears in the drawn design.
23
22
  */
@@ -42,8 +41,7 @@ let RulesConfigDesign = class RulesConfigDesign {
42
41
  exports.RulesConfigDesign = RulesConfigDesign;
43
42
  exports.RulesConfigDesign = RulesConfigDesign = tslib_1.__decorate([
44
43
  (0, di_1.DocumentDesign)(),
45
- (0, di_2.provideSingleton)(),
46
- (0, inversify_1.injectable)(),
44
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
47
45
  tslib_1.__metadata("design:paramtypes", [repo_root_1.RepoRootFinder,
48
46
  load_config_1.ConfigLoader,
49
47
  load_template_1.TemplateWriter,
@@ -1 +1 @@
1
- {"version":3,"file":"rules-config-design.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/rules-config-design.ts"],"names":[],"mappings":";;;;AAAA,6BAAsC;AACtC,6BAAwC;AACxC,yCAAuC;AAEvC,2CAA6C;AAC7C,+CAA6C;AAC7C,mDAAiD;AACjD,6CAAyC;AACzC,+DAA0D;AAC1D,+CAAkD;AAClD,yDAA2D;AAE3D;;;;;;;;GAQG;AAII,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAEL;IACA;IACA;IACA;IACA;IACA;IACA;IAPrB,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,cAA8B,EAC9B,SAAoB,EACpB,iBAAoC,EACpC,UAA6B,EAC7B,cAAqC;QANrC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,cAAS,GAAT,SAAS,CAAW;QACpB,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAmB;QAC7B,mBAAc,GAAd,cAAc,CAAuB;IACvD,CAAC;CACP,CAAA;AAVY,8CAAiB;4BAAjB,iBAAiB;IAH7B,IAAA,mBAAc,GAAE;IAChB,IAAA,qBAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;6CAG4B,0BAAc;QAChB,0BAAY;QACV,8BAAc;QACnB,sBAAS;QACD,uCAAiB;QACxB,+BAAiB;QACb,wCAAqB;GARjD,iBAAiB,CAU7B","sourcesContent":["import { DocumentDesign } from './di';\nimport { provideSingleton } from './di';\nimport { injectable } from 'inversify';\n\nimport { RepoRootFinder } from './repo-root';\nimport { ConfigLoader } from './load-config';\nimport { TemplateWriter } from './load-template';\nimport { DiffScope } from './diff-scope';\nimport { BranchMutationLog } from './branch-mutation-log';\nimport { ReviewJsonService } from './review-json';\nimport { MainSyncStatusService } from './main-sync-status';\n\n/**\n * DI-design root for @webpieces/rules-config (role:designed-lib).\n *\n * `@DocumentDesign` marks the top of the DAG the DI-design analyzer roots on, so the library's design\n * (design.json / design.md / design.html) is generated. rules-config is the shared foundation whose\n * utilities are being migrated from free functions to injected `@provideSingleton` service classes; as\n * each service class lands (config loader, template writer, diff/git services, …) it is injected HERE\n * so it appears in the drawn design.\n */\n@DocumentDesign()\n@provideSingleton()\n@injectable()\nexport class RulesConfigDesign {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly configLoader: ConfigLoader,\n private readonly templateWriter: TemplateWriter,\n private readonly diffScope: DiffScope,\n private readonly branchMutationLog: BranchMutationLog,\n private readonly reviewJson: ReviewJsonService,\n private readonly mainSyncStatus: MainSyncStatusService,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"rules-config-design.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/rules-config-design.ts"],"names":[],"mappings":";;;;AAAA,6BAAsC;AACtC,yCAA2D;AAE3D,2CAA6C;AAC7C,+CAA6C;AAC7C,mDAAiD;AACjD,6CAAyC;AACzC,+DAA0D;AAC1D,+CAAkD;AAClD,yDAA2D;AAE3D;;;;;;;;GAQG;AAGI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAEL;IACA;IACA;IACA;IACA;IACA;IACA;IAPrB,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,cAA8B,EAC9B,SAAoB,EACpB,iBAAoC,EACpC,UAA6B,EAC7B,cAAqC;QANrC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,cAAS,GAAT,SAAS,CAAW;QACpB,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAmB;QAC7B,mBAAc,GAAd,cAAc,CAAuB;IACvD,CAAC;CACP,CAAA;AAVY,8CAAiB;4BAAjB,iBAAiB;IAF7B,IAAA,mBAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,0BAAc;QAChB,0BAAY;QACV,8BAAc;QACnB,sBAAS;QACD,uCAAiB;QACxB,+BAAiB;QACb,wCAAqB;GARjD,iBAAiB,CAU7B","sourcesContent":["import { DocumentDesign } from './di';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { RepoRootFinder } from './repo-root';\nimport { ConfigLoader } from './load-config';\nimport { TemplateWriter } from './load-template';\nimport { DiffScope } from './diff-scope';\nimport { BranchMutationLog } from './branch-mutation-log';\nimport { ReviewJsonService } from './review-json';\nimport { MainSyncStatusService } from './main-sync-status';\n\n/**\n * DI-design root for @webpieces/rules-config (role:designed-lib).\n *\n * `@DocumentDesign` marks the top of the DAG the DI-design analyzer roots on, so the library's design\n * (design.json / design.md / design.html) is generated. rules-config is the shared foundation whose\n * utilities are being migrated from free functions to injected `@injectable(bindingScopeValues.Singleton)` service classes; as\n * each service class lands (config loader, template writer, diff/git services, …) it is injected HERE\n * so it appears in the drawn design.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class RulesConfigDesign {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly configLoader: ConfigLoader,\n private readonly templateWriter: TemplateWriter,\n private readonly diffScope: DiffScope,\n private readonly branchMutationLog: BranchMutationLog,\n private readonly reviewJson: ReviewJsonService,\n private readonly mainSyncStatus: MainSyncStatusService,\n ) {}\n}\n"]}
package/src/worktrees.js CHANGED
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorktreeService = exports.Worktree = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
- const di_1 = require("./di");
7
6
  const inversify_1 = require("inversify");
8
7
  /**
9
8
  * Reading the git worktree list.
@@ -127,7 +126,6 @@ let WorktreeService = class WorktreeService {
127
126
  };
128
127
  exports.WorktreeService = WorktreeService;
129
128
  exports.WorktreeService = WorktreeService = tslib_1.__decorate([
130
- (0, di_1.provideSingleton)(),
131
- (0, inversify_1.injectable)()
129
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
132
130
  ], WorktreeService);
133
131
  //# sourceMappingURL=worktrees.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { provideSingleton } from './di';\nimport { injectable } from 'inversify';\n\n/**\n * Reading the git worktree list.\n *\n * WHY this exists separately from merged-branches: a worktree is a SECOND budget. Every worktree holds\n * a branch, so if worktree-held branches also counted against the local-branch cap, five worktrees\n * would consume the entire branch budget and no branch could ever be created again. The two are\n * therefore counted apart — held branches against the worktree cap, parked branches against the branch\n * cap — and this service is what tells the two apart.\n *\n * Unlike the merged-PR lookup, everything here is LOCAL and cheap (one `git worktree list`), so it is\n * safe to call on the guard's blocking path.\n */\n\n// `git worktree list --porcelain` emits blank-line-separated records. The first record is always the\n// main worktree. Keys are space-separated; `detached`, `bare` and `locked` may appear bare (no value).\nconst WORKTREE_KEY = 'worktree ';\nconst BRANCH_KEY = 'branch ';\nconst REFS_HEADS = 'refs/heads/';\n\n// Data-only (per CLAUDE.md, classes for data).\nexport class Worktree {\n path: string;\n // Short branch name (refs/heads/ stripped). Empty when the worktree is detached or bare.\n branch: string;\n // The primary clone — the one that owns .git. Never counted against the cap, never removable.\n isMain: boolean;\n // git already knows this worktree's directory is gone; `git worktree prune` will clear it.\n prunable: boolean;\n // A human ran `git worktree lock`. Explicitly \"do not touch\".\n locked: boolean;\n\n constructor(path: string, branch: string, isMain: boolean, prunable: boolean, locked: boolean) {\n this.path = path;\n this.branch = branch;\n this.isMain = isMain;\n this.prunable = prunable;\n this.locked = locked;\n }\n}\n\n// Result of a captured git invocation: ok=false on spawn failure or non-zero exit.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n}\n\n@provideSingleton()\n@injectable()\nexport class WorktreeService {\n /**\n * Every worktree, main one first. Fails SOFT to [] — a repo with no worktree support, or a git\n * that errors, must read as \"no worktrees\" so the cap fails OPEN rather than blocking on data we\n * do not have.\n */\n listWorktrees(repoRoot: string): Worktree[] {\n const result = this.capture(repoRoot, ['worktree', 'list', '--porcelain']);\n if (!result.ok || result.out === '') return [];\n return this.parsePorcelain(result.out);\n }\n\n // The linked worktrees — everything except the primary clone. This is what the cap counts.\n linkedWorktrees(repoRoot: string): Worktree[] {\n return this.listWorktrees(repoRoot).filter((tree: Worktree): boolean => !tree.isMain);\n }\n\n /**\n * Branch names checked out in ANY worktree, including the primary clone's own HEAD.\n *\n * Two callers, one reason: git flatly refuses to delete a branch that is checked out somewhere.\n * A held branch must never be proposed for `git branch -D` (the delete would fail and take the\n * whole reap command down with it), and it must not be counted as a parked branch either.\n */\n heldBranches(repoRoot: string): Set<string> {\n const held = new Set<string>();\n for (const tree of this.listWorktrees(repoRoot)) {\n if (tree.branch !== '') held.add(tree.branch);\n }\n return held;\n }\n\n /**\n * Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank\n * line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree\n * still appears in the list, which is exactly why it can be reaped.\n */\n private parsePorcelain(out: string): Worktree[] {\n const trees: Worktree[] = [];\n let path = '';\n let branch = '';\n let prunable = false;\n let locked = false;\n\n const flush = (): void => {\n if (path === '') return;\n trees.push(new Worktree(path, branch, trees.length === 0, prunable, locked));\n path = '';\n branch = '';\n prunable = false;\n locked = false;\n };\n\n for (const raw of out.split('\\n')) {\n const line = raw.trim();\n if (line === '') {\n flush();\n } else if (line.startsWith(WORKTREE_KEY)) {\n // A new record begins — flush the previous one in case the blank line was missing.\n flush();\n path = line.slice(WORKTREE_KEY.length).trim();\n } else if (line.startsWith(BRANCH_KEY)) {\n const ref = line.slice(BRANCH_KEY.length).trim();\n branch = ref.startsWith(REFS_HEADS) ? ref.slice(REFS_HEADS.length) : ref;\n } else if (line === 'prunable' || line.startsWith('prunable ')) {\n prunable = true;\n } else if (line === 'locked' || line.startsWith('locked ')) {\n locked = true;\n }\n }\n flush();\n\n return trees;\n }\n\n // Run a git command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return { ok: false, out: '' };\n return { ok: true, out: result.stdout.trim() };\n }\n}\n"]}
1
+ {"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Reading the git worktree list.\n *\n * WHY this exists separately from merged-branches: a worktree is a SECOND budget. Every worktree holds\n * a branch, so if worktree-held branches also counted against the local-branch cap, five worktrees\n * would consume the entire branch budget and no branch could ever be created again. The two are\n * therefore counted apart — held branches against the worktree cap, parked branches against the branch\n * cap — and this service is what tells the two apart.\n *\n * Unlike the merged-PR lookup, everything here is LOCAL and cheap (one `git worktree list`), so it is\n * safe to call on the guard's blocking path.\n */\n\n// `git worktree list --porcelain` emits blank-line-separated records. The first record is always the\n// main worktree. Keys are space-separated; `detached`, `bare` and `locked` may appear bare (no value).\nconst WORKTREE_KEY = 'worktree ';\nconst BRANCH_KEY = 'branch ';\nconst REFS_HEADS = 'refs/heads/';\n\n// Data-only (per CLAUDE.md, classes for data).\nexport class Worktree {\n path: string;\n // Short branch name (refs/heads/ stripped). Empty when the worktree is detached or bare.\n branch: string;\n // The primary clone — the one that owns .git. Never counted against the cap, never removable.\n isMain: boolean;\n // git already knows this worktree's directory is gone; `git worktree prune` will clear it.\n prunable: boolean;\n // A human ran `git worktree lock`. Explicitly \"do not touch\".\n locked: boolean;\n\n constructor(path: string, branch: string, isMain: boolean, prunable: boolean, locked: boolean) {\n this.path = path;\n this.branch = branch;\n this.isMain = isMain;\n this.prunable = prunable;\n this.locked = locked;\n }\n}\n\n// Result of a captured git invocation: ok=false on spawn failure or non-zero exit.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeService {\n /**\n * Every worktree, main one first. Fails SOFT to [] — a repo with no worktree support, or a git\n * that errors, must read as \"no worktrees\" so the cap fails OPEN rather than blocking on data we\n * do not have.\n */\n listWorktrees(repoRoot: string): Worktree[] {\n const result = this.capture(repoRoot, ['worktree', 'list', '--porcelain']);\n if (!result.ok || result.out === '') return [];\n return this.parsePorcelain(result.out);\n }\n\n // The linked worktrees — everything except the primary clone. This is what the cap counts.\n linkedWorktrees(repoRoot: string): Worktree[] {\n return this.listWorktrees(repoRoot).filter((tree: Worktree): boolean => !tree.isMain);\n }\n\n /**\n * Branch names checked out in ANY worktree, including the primary clone's own HEAD.\n *\n * Two callers, one reason: git flatly refuses to delete a branch that is checked out somewhere.\n * A held branch must never be proposed for `git branch -D` (the delete would fail and take the\n * whole reap command down with it), and it must not be counted as a parked branch either.\n */\n heldBranches(repoRoot: string): Set<string> {\n const held = new Set<string>();\n for (const tree of this.listWorktrees(repoRoot)) {\n if (tree.branch !== '') held.add(tree.branch);\n }\n return held;\n }\n\n /**\n * Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank\n * line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree\n * still appears in the list, which is exactly why it can be reaped.\n */\n private parsePorcelain(out: string): Worktree[] {\n const trees: Worktree[] = [];\n let path = '';\n let branch = '';\n let prunable = false;\n let locked = false;\n\n const flush = (): void => {\n if (path === '') return;\n trees.push(new Worktree(path, branch, trees.length === 0, prunable, locked));\n path = '';\n branch = '';\n prunable = false;\n locked = false;\n };\n\n for (const raw of out.split('\\n')) {\n const line = raw.trim();\n if (line === '') {\n flush();\n } else if (line.startsWith(WORKTREE_KEY)) {\n // A new record begins — flush the previous one in case the blank line was missing.\n flush();\n path = line.slice(WORKTREE_KEY.length).trim();\n } else if (line.startsWith(BRANCH_KEY)) {\n const ref = line.slice(BRANCH_KEY.length).trim();\n branch = ref.startsWith(REFS_HEADS) ? ref.slice(REFS_HEADS.length) : ref;\n } else if (line === 'prunable' || line.startsWith('prunable ')) {\n prunable = true;\n } else if (line === 'locked' || line.startsWith('locked ')) {\n locked = true;\n }\n }\n flush();\n\n return trees;\n }\n\n // Run a git command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return { ok: false, out: '' };\n return { ok: true, out: result.stdout.trim() };\n }\n}\n"]}