@pieai/pro-gov 0.4.8 → 0.5.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/cli.js CHANGED
@@ -1654,9 +1654,9 @@ import { createRequire } from "node:module";
1654
1654
  import { dirname as dirname6, join as join12 } from "node:path";
1655
1655
  var REQUIRED_ASSETS = [
1656
1656
  "starter/AGENTS.template.md",
1657
- "starter/docs/governance/ssot-v0.9.md",
1658
- "starter/docs/governance/agents-routing/engineering-runtime-v0.9.md",
1659
- "starter/docs/governance/agents-routing/doc-only-v0.9.md",
1657
+ "starter/docs/governance/ssot-v1.0.md",
1658
+ "starter/docs/governance/agents-routing/engineering-runtime-v1.0.md",
1659
+ "starter/docs/governance/agents-routing/doc-only-v1.0.md",
1660
1660
  "profiles/engineering-runtime/profile.md",
1661
1661
  "profiles/doc-only/profile.md"
1662
1662
  ];
@@ -1738,7 +1738,7 @@ function classifyOwnership(targetPath) {
1738
1738
  return "shared";
1739
1739
  }
1740
1740
  function isOtherProfileRouting(targetPath, profile) {
1741
- return targetPath.startsWith("docs/governance/agents-routing/") && targetPath !== `docs/governance/agents-routing/${profile}-v0.9.md`;
1741
+ return targetPath.startsWith("docs/governance/agents-routing/") && targetPath !== `docs/governance/agents-routing/${profile}-v1.0.md`;
1742
1742
  }
1743
1743
  function starterTargetPath(sourcePath) {
1744
1744
  if (sourcePath === "starter/AGENTS.template.md") return "AGENTS.md";
@@ -1800,7 +1800,7 @@ function applyStarterFiles(files, profile) {
1800
1800
  return 0;
1801
1801
  }
1802
1802
  function renderAgentsTemplate(template, projectName, profile) {
1803
- const selectedRoute = `docs/governance/agents-routing/${profile}-v0.9.md`;
1803
+ const selectedRoute = `docs/governance/agents-routing/${profile}-v1.0.md`;
1804
1804
  return template.replace("# PROJECT_NAME AI Router", `# ${projectName} AI Router`).replace("`PROFILE_NAME`", `\`${profile}\``).replace("`PROFILE_ROUTE`", `\`${selectedRoute}\``).replace(/\n{3,}/g, "\n\n");
1805
1805
  }
1806
1806
  function readFlag(args, flag) {
@@ -1834,18 +1834,22 @@ function recallLearnings(root, options) {
1834
1834
  return { query, hits };
1835
1835
  }
1836
1836
  function loadLearningRecords(root) {
1837
- const records = [];
1838
- const solutionsDir = join14(root, "docs/solutions");
1839
- if (existsSync14(solutionsDir)) {
1840
- for (const path of listMarkdownFiles(solutionsDir)) {
1841
- records.push(readLearningRecord(root, path));
1837
+ const recordsByTitle = /* @__PURE__ */ new Map();
1838
+ for (const relativeDir of ["docs/reference/learnings", "docs/solutions"]) {
1839
+ const learningDir = join14(root, relativeDir);
1840
+ if (!existsSync14(learningDir)) continue;
1841
+ for (const path of listMarkdownFiles(learningDir)) {
1842
+ const record = readLearningRecord(root, path);
1843
+ const key = record.title.trim().toLowerCase();
1844
+ if (!recordsByTitle.has(key)) recordsByTitle.set(key, record);
1842
1845
  }
1843
1846
  }
1844
1847
  const conceptsPath = join14(root, "CONCEPTS.md");
1845
1848
  if (existsSync14(conceptsPath)) {
1846
- records.push(readLearningRecord(root, conceptsPath));
1849
+ const record = readLearningRecord(root, conceptsPath);
1850
+ recordsByTitle.set(`concepts:${record.title.toLowerCase()}`, record);
1847
1851
  }
1848
- return records;
1852
+ return [...recordsByTitle.values()];
1849
1853
  }
1850
1854
  function listMarkdownFiles(dir) {
1851
1855
  const files = [];
@@ -1944,7 +1948,7 @@ function cleanMarkdownLine(input) {
1944
1948
 
1945
1949
  // src/learning/capture.ts
1946
1950
  import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
1947
- import { join as join15, relative as relative6 } from "node:path";
1951
+ import { basename as basename5, join as join15, relative as relative6 } from "node:path";
1948
1952
  function captureLearning(root, options) {
1949
1953
  const title = options.title.trim();
1950
1954
  const summary = options.summary.trim();
@@ -1952,10 +1956,11 @@ function captureLearning(root, options) {
1952
1956
  if (!summary) throw new Error("summary is required");
1953
1957
  const category = slugify(options.category ?? "workflow-issues") || "workflow-issues";
1954
1958
  const moduleName = options.module?.trim() || "PGS learning capture";
1955
- const dir = join15(root, "docs/solutions", category);
1959
+ const dir = join15(root, "docs/reference/learnings", category);
1956
1960
  mkdirSync5(dir, { recursive: true });
1957
1961
  const path = uniquePath(dir, slugify(title) || "learning");
1958
- writeFileSync4(path, renderLearning({ title, summary, category, moduleName }));
1962
+ const idSlug = basename5(path, ".md");
1963
+ writeFileSync4(path, renderLearning({ title, summary, category, moduleName, idSlug }));
1959
1964
  return {
1960
1965
  relativePath: normalizePath2(relative6(root, path)),
1961
1966
  title,
@@ -1965,12 +1970,23 @@ function captureLearning(root, options) {
1965
1970
  function renderLearning(options) {
1966
1971
  return [
1967
1972
  "---",
1973
+ `id: REF-LEARNING-${upperSlug(options.category)}-${upperSlug(options.idSlug)}`,
1968
1974
  `title: ${yamlString(options.title)}`,
1969
- `date: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
1975
+ "type: reference",
1976
+ "status: stable",
1977
+ "canonical: true",
1978
+ "owner: ai-assisted",
1979
+ `created: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
1980
+ `last_reviewed: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
1981
+ "domain: learning",
1982
+ "tags:",
1983
+ " - learning-recall",
1984
+ ` - ${options.category}`,
1985
+ "pinned: false",
1986
+ "related: []",
1970
1987
  `category: ${options.category}`,
1971
1988
  `module: ${yamlString(options.moduleName)}`,
1972
1989
  "capture_mode: pgs-native",
1973
- "tags: [learning-recall]",
1974
1990
  "---",
1975
1991
  "",
1976
1992
  `# ${options.title}`,
@@ -2004,6 +2020,9 @@ function yamlString(input) {
2004
2020
  function normalizePath2(path) {
2005
2021
  return path.split("\\").join("/");
2006
2022
  }
2023
+ function upperSlug(input) {
2024
+ return slugify(input).toUpperCase() || "LEARNING";
2025
+ }
2007
2026
 
2008
2027
  // src/commands/learn.ts
2009
2028
  function runLearn(args) {
@@ -2173,7 +2192,7 @@ import { dirname as dirname9 } from "node:path";
2173
2192
 
2174
2193
  // src/lens/audit.ts
2175
2194
  import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
2176
- import { basename as basename5, dirname as dirname8, join as join16 } from "node:path";
2195
+ import { basename as basename6, dirname as dirname8, join as join16 } from "node:path";
2177
2196
  var REQUIRED_ARTIFACTS = [
2178
2197
  "manifest.md",
2179
2198
  "raw/project-lens/architecture-lens.md",
@@ -2191,7 +2210,7 @@ function createProjectLensAuditPackage(targetDir, auditDir) {
2191
2210
  version: 1,
2192
2211
  target: {
2193
2212
  path: targetDir,
2194
- name: basename5(targetDir) || "target"
2213
+ name: basename6(targetDir) || "target"
2195
2214
  },
2196
2215
  requiredArtifacts: [...REQUIRED_ARTIFACTS]
2197
2216
  };
@@ -2824,6 +2843,7 @@ function resolveManifestPaths(value, configDir) {
2824
2843
  };
2825
2844
  return {
2826
2845
  ...value,
2846
+ technologyGovernance: isRecord(value.technologyGovernance) && typeof value.technologyGovernance.strategySource === "string" && !isAbsolute3(value.technologyGovernance.strategySource) ? { ...value.technologyGovernance, strategySource: resolve3(configDir, value.technologyGovernance.strategySource) } : value.technologyGovernance,
2827
2847
  controlPlane: resolveEndpoint(value.controlPlane),
2828
2848
  executionEngine: resolveEndpoint(value.executionEngine),
2829
2849
  targets: Array.isArray(value.targets) ? value.targets.map(resolveEndpoint) : value.targets
@@ -2857,11 +2877,12 @@ function validatePortfolioManifest(value) {
2857
2877
  validateAllowedFields(
2858
2878
  value,
2859
2879
  "root",
2860
- ["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "hostTooling", "targets"],
2880
+ ["schemaVersion", "portfolioId", "technologyGovernance", "controlPlane", "executionEngine", "hostTooling", "targets"],
2861
2881
  issues
2862
2882
  );
2863
- validateEndpoint(value.controlPlane, "controlPlane", issues);
2864
- validateEndpoint(value.executionEngine, "executionEngine", issues);
2883
+ const technologyCatalog = validateTechnologyGovernance(value.technologyGovernance, issues);
2884
+ validateEndpoint(value.controlPlane, "controlPlane", issues, technologyCatalog);
2885
+ validateEndpoint(value.executionEngine, "executionEngine", issues, technologyCatalog);
2865
2886
  validateHostTooling(value.hostTooling, issues);
2866
2887
  if (!Array.isArray(value.targets)) {
2867
2888
  issues.push({
@@ -2881,8 +2902,8 @@ function validatePortfolioManifest(value) {
2881
2902
  });
2882
2903
  continue;
2883
2904
  }
2884
- validateEndpoint(target, "targets", issues);
2885
- validateAllowedFields(target, "target", ["id", "path", "profile", "assetBundles"], issues);
2905
+ validateEndpoint(target, "targets", issues, technologyCatalog);
2906
+ validateAllowedFields(target, "target", ["id", "path", "profile", "assetBundles", "projectType", "capabilities", "boundary", "technologyExceptions"], issues);
2886
2907
  if (typeof target.id === "string") {
2887
2908
  if (seenTargetIds.has(target.id)) {
2888
2909
  issues.push({
@@ -2928,7 +2949,7 @@ function validateAllowedFields(value, location, allowedFields, issues) {
2928
2949
  });
2929
2950
  }
2930
2951
  }
2931
- function validateEndpoint(value, field, issues) {
2952
+ function validateEndpoint(value, field, issues, technologyCatalog) {
2932
2953
  if (value === void 0) return;
2933
2954
  if (!isRecord(value)) {
2934
2955
  issues.push({
@@ -2938,6 +2959,12 @@ function validateEndpoint(value, field, issues) {
2938
2959
  });
2939
2960
  return;
2940
2961
  }
2962
+ validateAllowedFields(
2963
+ value,
2964
+ field,
2965
+ field === "targets" ? ["id", "path", "profile", "assetBundles", "projectType", "capabilities", "boundary", "technologyExceptions"] : ["id", "path", "projectType", "capabilities", "boundary", "technologyExceptions"],
2966
+ issues
2967
+ );
2941
2968
  if (typeof value.id !== "string" || value.id.length === 0) {
2942
2969
  issues.push({
2943
2970
  type: "invalid-field",
@@ -2962,6 +2989,7 @@ function validateEndpoint(value, field, issues) {
2962
2989
  message: `Portfolio ${field} path does not exist: ${value.path}`
2963
2990
  });
2964
2991
  }
2992
+ validateRepositoryGovernance(value, field, technologyCatalog, issues);
2965
2993
  }
2966
2994
  function validateOptionalStringArray(value, id, field, issues) {
2967
2995
  if (value === void 0) return;
@@ -2974,6 +3002,118 @@ function validateOptionalStringArray(value, id, field, issues) {
2974
3002
  });
2975
3003
  }
2976
3004
  }
3005
+ function validateTechnologyGovernance(value, issues) {
3006
+ const technologies = /* @__PURE__ */ new Set();
3007
+ const projectTypes = /* @__PURE__ */ new Map();
3008
+ if (value === void 0) return { technologies, projectTypes };
3009
+ if (!isRecord(value)) {
3010
+ issues.push({ type: "invalid-field", field: "technologyGovernance", message: "Portfolio technologyGovernance must be an object." });
3011
+ return { technologies, projectTypes };
3012
+ }
3013
+ validateAllowedFields(value, "technologyGovernance", ["strategySource", "technologies", "projectTypes"], issues);
3014
+ if (value.strategySource !== void 0 && (typeof value.strategySource !== "string" || value.strategySource.length === 0)) {
3015
+ issues.push({ type: "invalid-field", field: "technologyGovernance.strategySource", message: "Technology strategySource must be a non-empty string." });
3016
+ }
3017
+ if (!Array.isArray(value.technologies)) {
3018
+ issues.push({ type: "invalid-field", field: "technologyGovernance.technologies", message: "Technology definitions must be an array." });
3019
+ } else {
3020
+ for (const technology of value.technologies) {
3021
+ if (!isRecord(technology)) {
3022
+ issues.push({ type: "invalid-field", field: "technologyGovernance.technologies", message: "Technology definition must be an object." });
3023
+ continue;
3024
+ }
3025
+ validateAllowedFields(technology, "technology", ["id", "label", "packages", "files"], issues);
3026
+ if (typeof technology.id !== "string" || technology.id.length === 0 || technologies.has(technology.id)) {
3027
+ issues.push({ type: "invalid-field", field: "technologyGovernance.technologies.id", message: `Technology id must be non-empty and unique: ${String(technology.id)}` });
3028
+ } else technologies.add(technology.id);
3029
+ if (typeof technology.label !== "string" || technology.label.length === 0) {
3030
+ issues.push({ type: "invalid-field", field: "technologyGovernance.technologies.label", message: "Technology label must be a non-empty string." });
3031
+ }
3032
+ validateOptionalStringArray(technology.packages, technology.id, "technology.packages", issues);
3033
+ validateOptionalStringArray(technology.files, technology.id, "technology.files", issues);
3034
+ if (Array.isArray(technology.files)) {
3035
+ for (const file of technology.files) {
3036
+ if (typeof file === "string" && !isRepositoryRelativePath(file)) {
3037
+ issues.push({
3038
+ type: "invalid-field",
3039
+ id: typeof technology.id === "string" ? technology.id : void 0,
3040
+ field: "technology.files",
3041
+ message: `Technology file signal must stay inside the repository: ${file}`
3042
+ });
3043
+ }
3044
+ }
3045
+ }
3046
+ }
3047
+ }
3048
+ if (!Array.isArray(value.projectTypes)) {
3049
+ issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: "Project type definitions must be an array." });
3050
+ } else {
3051
+ for (const projectType of value.projectTypes) {
3052
+ if (!isRecord(projectType)) {
3053
+ issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: "Project type definition must be an object." });
3054
+ continue;
3055
+ }
3056
+ validateAllowedFields(projectType, "projectType", ["id", "title", "summary", "baseline", "optionalCapabilities"], issues);
3057
+ const id = typeof projectType.id === "string" ? projectType.id : "";
3058
+ if (!id || projectTypes.has(id)) {
3059
+ issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes.id", message: `Project type id must be non-empty and unique: ${String(projectType.id)}` });
3060
+ }
3061
+ if (typeof projectType.title !== "string" || typeof projectType.summary !== "string") {
3062
+ issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: `Project type ${id || "(unknown)"} requires title and summary.` });
3063
+ }
3064
+ validateOptionalStringArray(projectType.baseline, id, "projectType.baseline", issues);
3065
+ validateOptionalStringArray(projectType.optionalCapabilities, id, "projectType.optionalCapabilities", issues);
3066
+ if (id && typeof projectType.title === "string" && typeof projectType.summary === "string" && Array.isArray(projectType.baseline) && Array.isArray(projectType.optionalCapabilities)) {
3067
+ projectTypes.set(id, projectType);
3068
+ }
3069
+ }
3070
+ }
3071
+ for (const projectType of projectTypes.values()) {
3072
+ for (const technology of [...projectType.baseline, ...projectType.optionalCapabilities]) {
3073
+ if (!technologies.has(technology)) issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: `Project type ${projectType.id} references unknown technology: ${technology}` });
3074
+ }
3075
+ }
3076
+ return { technologies, projectTypes };
3077
+ }
3078
+ function isRepositoryRelativePath(value) {
3079
+ if (value.length === 0 || isAbsolute3(value)) return false;
3080
+ const segments = value.replaceAll("\\", "/").split("/");
3081
+ return !segments.includes("..");
3082
+ }
3083
+ function validateRepositoryGovernance(value, field, catalog, issues) {
3084
+ const id = typeof value.id === "string" ? value.id : void 0;
3085
+ if (value.projectType !== void 0 && (typeof value.projectType !== "string" || !catalog.projectTypes.has(value.projectType))) {
3086
+ issues.push({ type: "invalid-field", id, field: `${field}.projectType`, message: `Repository projectType is not defined: ${String(value.projectType)}` });
3087
+ }
3088
+ validateOptionalStringArray(value.capabilities, id, "capabilities", issues);
3089
+ if (Array.isArray(value.capabilities) && typeof value.projectType === "string") {
3090
+ const allowed = new Set(catalog.projectTypes.get(value.projectType)?.optionalCapabilities ?? []);
3091
+ for (const capability of value.capabilities) {
3092
+ if (typeof capability === "string" && !allowed.has(capability)) issues.push({ type: "invalid-field", id, field: `${field}.capabilities`, message: `Capability ${capability} is not optional for project type ${value.projectType}.` });
3093
+ }
3094
+ }
3095
+ if (value.boundary !== void 0) {
3096
+ if (!isRecord(value.boundary)) issues.push({ type: "invalid-field", id, field: `${field}.boundary`, message: "Repository boundary must be an object." });
3097
+ else {
3098
+ validateAllowedFields(value.boundary, "boundary", ["owns", "doesNotOwn"], issues);
3099
+ validateOptionalStringArray(value.boundary.owns, id, "boundary.owns", issues);
3100
+ validateOptionalStringArray(value.boundary.doesNotOwn, id, "boundary.doesNotOwn", issues);
3101
+ }
3102
+ }
3103
+ if (value.technologyExceptions !== void 0) {
3104
+ if (!Array.isArray(value.technologyExceptions)) issues.push({ type: "invalid-field", id, field: `${field}.technologyExceptions`, message: "Technology exceptions must be an array." });
3105
+ else for (const exception of value.technologyExceptions) {
3106
+ if (!isRecord(exception)) {
3107
+ issues.push({ type: "invalid-field", id, field: `${field}.technologyExceptions`, message: "Technology exception must be an object." });
3108
+ continue;
3109
+ }
3110
+ validateAllowedFields(exception, "technologyException", ["technology", "classification", "reason"], issues);
3111
+ if (typeof exception.technology !== "string" || !catalog.technologies.has(exception.technology)) issues.push({ type: "invalid-field", id, field: `${field}.technologyExceptions.technology`, message: `Technology exception references unknown technology: ${String(exception.technology)}` });
3112
+ if (!["acceptable-exception", "observe", "scheduled-migration", "urgent-drift", "bad-standard"].includes(String(exception.classification))) issues.push({ type: "invalid-field", id, field: `${field}.technologyExceptions.classification`, message: `Unsupported technology exception classification: ${String(exception.classification)}` });
3113
+ if (typeof exception.reason !== "string" || exception.reason.length === 0) issues.push({ type: "invalid-field", id, field: `${field}.technologyExceptions.reason`, message: "Technology exception reason must be non-empty." });
3114
+ }
3115
+ }
3116
+ }
2977
3117
  function validateHostTooling(value, issues) {
2978
3118
  if (value === void 0) return;
2979
3119
  if (!Array.isArray(value)) {
@@ -3390,7 +3530,7 @@ import {
3390
3530
  statSync as statSync4,
3391
3531
  writeFileSync as writeFileSync7
3392
3532
  } from "node:fs";
3393
- import { dirname as dirname12, join as join20, relative as relative8, resolve as resolve4 } from "node:path";
3533
+ import { dirname as dirname12, join as join20, relative as relative8, resolve as resolve4, sep } from "node:path";
3394
3534
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3395
3535
  function inspectPortfolioAiHealth(options) {
3396
3536
  const endpoints = collectEndpoints(options.manifest);
@@ -3399,7 +3539,7 @@ function inspectPortfolioAiHealth(options) {
3399
3539
  const executionEngineRoot = options.manifest.executionEngine?.path;
3400
3540
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3401
3541
  const expectedPackageVersion = packageVersion(join20(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3402
- const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion));
3542
+ const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion, options.manifest.technologyGovernance));
3403
3543
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3404
3544
  for (const repository of repositories) summary[repository.status] += 1;
3405
3545
  return {
@@ -3410,6 +3550,11 @@ function inspectPortfolioAiHealth(options) {
3410
3550
  secretsRoot: inspectSecretsRoot(secretsRoot),
3411
3551
  userMcp: inspectUserMcp(homeDir),
3412
3552
  skillRegistry,
3553
+ technologyGovernance: {
3554
+ strategySource: options.manifest.technologyGovernance?.strategySource,
3555
+ projectTypes: options.manifest.technologyGovernance?.projectTypes.length ?? 0,
3556
+ technologies: options.manifest.technologyGovernance?.technologies.length ?? 0
3557
+ },
3413
3558
  summary,
3414
3559
  repositories
3415
3560
  };
@@ -3443,19 +3588,20 @@ function collectEndpoints(manifest) {
3443
3588
  return true;
3444
3589
  });
3445
3590
  }
3446
- function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion) {
3591
+ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion, technologyGovernance) {
3447
3592
  const root = endpoint.path;
3448
3593
  const git = inspectGit2(root);
3449
3594
  const entries = inspectEntries(root);
3450
3595
  const skills = inspectSkills(root);
3451
3596
  const hooks = inspectHooks(root);
3452
- const docs = inspectDocs(root, expectedPackageVersion);
3597
+ const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3453
3598
  const mcp = {
3454
3599
  root: jsonObjectKeys(join20(root, ".mcp.json"), "mcpServers"),
3455
3600
  claudeCode: jsonObjectKeys(join20(root, ".claude/settings.json"), "mcpServers"),
3456
3601
  codex: tomlMcpNames(join20(root, ".codex/config.toml"))
3457
3602
  };
3458
3603
  const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository);
3604
+ const projectModel = inspectProjectModel(root, endpoint, technologyGovernance);
3459
3605
  const recommendations = [];
3460
3606
  if (!git.isRepository) recommendations.push("\u8BE5\u8DEF\u5F84\u4E0D\u662F Git \u4ED3\u5E93\uFF1B\u786E\u8BA4\u6E05\u5355\u8DEF\u5F84\u662F\u5426\u6B63\u786E\u3002");
3461
3607
  if (git.unmergedBranches.length > 0) recommendations.push(`\u6709 ${git.unmergedBranches.length} \u6761\u5206\u652F\u5C1A\u672A\u5408\u5165\u5F53\u524D HEAD\uFF1A${git.unmergedBranches.join(", ")}\u3002`);
@@ -3475,17 +3621,24 @@ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion)
3475
3621
  if (skills.claudeCompatibility === "duplicate-directory") recommendations.push("`.claude/skills` \u662F\u72EC\u7ACB\u526F\u672C\uFF1B\u5EFA\u8BAE\u94FE\u63A5\u5230 `.agents/skills`\uFF0C\u907F\u514D\u53CC\u4EFD\u6280\u80FD\u6F02\u79FB\u3002");
3476
3622
  if (skills.claudeCompatibility === "dangling-symlink") recommendations.push("`.claude/skills` \u662F\u65AD\u5F00\u7684\u94FE\u63A5\u3002");
3477
3623
  if (hasWorkflowReminderHooks(hooks)) recommendations.push("\u53D1\u73B0 Stop/SubagentStop hook\uFF1B\u786E\u8BA4\u5B83\u662F\u5426\u4ECD\u6709\u9879\u76EE\u4E13\u5C5E\u7528\u9014\uFF0C\u5E76\u79FB\u9664\u9000\u4F11\u7684 PGS \u5DE5\u4F5C\u6D41\u63D0\u9192\u3002");
3478
- const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template);
3624
+ const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3479
3625
  if (liveEnv.some((file) => file.tracked)) recommendations.push("\u53D1\u73B0\u88AB Git \u8DDF\u8E2A\u7684\u771F\u5B9E\u73AF\u5883\u6587\u4EF6\uFF1B\u5E94\u7ACB\u5373\u786E\u8BA4\u5176\u4E2D\u662F\u5426\u5305\u542B\u51ED\u636E\u5E76\u8FC1\u51FA\u4ED3\u5E93\u3002");
3480
- else if (liveEnv.length > 0 && secrets.centralDirectory === "absent") recommendations.push("\u4ED3\u5E93\u6709\u672C\u5730\u73AF\u5883\u6587\u4EF6\uFF0C\u4F46\u4E2D\u592E `.secrets` \u4E2D\u6CA1\u6709\u5BF9\u5E94\u76EE\u5F55\uFF1B\u786E\u8BA4\u662F\u5426\u9700\u8981\u7EB3\u5165\u5206\u5C42\u7BA1\u7406\u3002");
3626
+ if (liveEnv.length > 0 && secrets.centralDirectory === "absent") recommendations.push("\u4ED3\u5E93\u6709\u672C\u5730\u73AF\u5883\u6587\u4EF6\uFF0C\u4F46\u4E2D\u592E `.secrets` \u4E2D\u6CA1\u6709\u5BF9\u5E94\u76EE\u5F55\uFF1B\u786E\u8BA4\u662F\u5426\u9700\u8981\u7EB3\u5165\u5206\u5C42\u7BA1\u7406\u3002");
3627
+ else if (liveEnv.some((file) => !file.centralized)) recommendations.push("\u53D1\u73B0\u771F\u5B9E\u73AF\u5883\u6587\u4EF6\u5C1A\u672A\u8FDE\u63A5\u5230\u672C\u9879\u76EE\u7684\u4E2D\u592E `.secrets` \u76EE\u5F55\uFF1B\u533A\u5206\u53EF\u4E22\u5F03\u751F\u6210\u7269\u4E0E\u672C\u5730\u4E3B\u6765\u6E90\uFF0C\u9700\u4FDD\u7559\u7684\u6765\u6E90\u5E94\u96C6\u4E2D\u540E\u518D\u63A5\u56DE\u9879\u76EE\u3002");
3628
+ if (hasUnsafeCentralSecretPermissions(secrets)) recommendations.push("\u4E2D\u592E\u5BC6\u94A5\u76EE\u5F55\u6216\u6587\u4EF6\u6743\u9650\u8FC7\u5BBD\uFF1B\u76EE\u5F55\u5E94\u4E3A 0700\uFF0C\u914D\u7F6E\u6587\u4EF6\u5E94\u4E3A 0600\u3002");
3481
3629
  if (!docs.packages.aligned) recommendations.push(`PGS \u5305\u7248\u672C\u672A\u4E0E\u6267\u884C\u5F15\u64CE ${docs.packages.expected ?? "\u672A\u77E5\u7248\u672C"} \u5BF9\u9F50\uFF1B\u53D1\u5E03\u4E0A\u6E38\u540E\u518D\u540C\u6B65\u76EE\u6807\u4ED3\u5E93\u3002`);
3482
3630
  if (role === "target" && !docs.manifest) recommendations.push("\u7F3A\u5C11 docs/governance/MANIFEST.yml\uFF1B\u6587\u6863\u6E05\u5355\u65E0\u6CD5\u8BC1\u660E\u5DF2\u540C\u6B65\u3002");
3631
+ if (technologyGovernance && !projectModel.projectType) recommendations.push("\u672A\u58F0\u660E\u9879\u76EE\u7C7B\u578B\uFF1B\u65E0\u6CD5\u628A\u5B9E\u9645\u6280\u672F\u4E0E\u4EA7\u54C1\u7EBF\u57FA\u7EBF\u8FDB\u884C\u6BD4\u8F83\u3002");
3632
+ const missingBaseline = projectModel.baseline.filter((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3633
+ if (missingBaseline.length > 0) recommendations.push(`\u6280\u672F\u57FA\u7EBF\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingBaseline.map((technology) => technology.label).join("\u3001")}\u3002`);
3634
+ const missingSelected = projectModel.optionalCapabilities.filter((technology) => technology.selected && !technology.detected);
3635
+ if (missingSelected.length > 0) recommendations.push(`\u5DF2\u9009\u80FD\u529B\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingSelected.map((technology) => technology.label).join("\u3001")}\u3002`);
3483
3636
  return {
3484
3637
  id: endpoint.id,
3485
3638
  role,
3486
3639
  path: root,
3487
3640
  profile: "profile" in endpoint ? endpoint.profile : void 0,
3488
- status: deriveStatus(entries, git, hooks, skills, secrets),
3641
+ status: deriveStatus(entries, git, hooks, skills, secrets, projectModel, Boolean(technologyGovernance)),
3489
3642
  recommendations,
3490
3643
  git,
3491
3644
  entries,
@@ -3493,14 +3646,62 @@ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion)
3493
3646
  mcp,
3494
3647
  skills,
3495
3648
  secrets,
3496
- docs
3649
+ docs,
3650
+ projectModel
3497
3651
  };
3498
3652
  }
3499
- function deriveStatus(entries, git, hooks, skills, secrets) {
3500
- if (!git.isRepository || entries.agents === "missing" || entries.claude === "dangling-symlink" || entries.gemini === "dangling-symlink" || skills.canonical.some((item) => item.kind === "dangling-symlink") || secrets.repositoryEnvFiles.some((file) => file.tracked && !file.template)) return "unhealthy";
3501
- if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0) return "attention";
3653
+ function deriveStatus(entries, git, hooks, skills, secrets, projectModel, technologyGovernanceConfigured) {
3654
+ if (!git.isRepository || entries.agents === "missing" || entries.claude === "dangling-symlink" || entries.gemini === "dangling-symlink" || skills.canonical.some((item) => item.kind === "dangling-symlink") || secrets.repositoryEnvFiles.some((file) => file.tracked && !file.template && !file.fixture) || hasUnsafeCentralSecretPermissions(secrets)) return "unhealthy";
3655
+ const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3656
+ const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
3657
+ const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3658
+ const secretMaterializationNeedsReview = liveEnv.some((file) => !file.centralized);
3659
+ if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected) return "attention";
3502
3660
  return "healthy";
3503
3661
  }
3662
+ function inspectProjectModel(root, endpoint, governance) {
3663
+ const projectType = governance?.projectTypes.find((candidate) => candidate.id === endpoint.projectType);
3664
+ const packages = collectPackageNames(root);
3665
+ const technologyById = new Map((governance?.technologies ?? []).map((technology) => [technology.id, technology]));
3666
+ const detection = (id) => {
3667
+ const technology = technologyById.get(id);
3668
+ const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
3669
+ const fileMatch = technology?.files?.some((path) => existsSync21(join20(root, path))) ?? false;
3670
+ return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
3671
+ };
3672
+ const selected = new Set(endpoint.capabilities ?? []);
3673
+ return {
3674
+ projectType: projectType ? { id: projectType.id, title: projectType.title, summary: projectType.summary } : void 0,
3675
+ boundary: endpoint.boundary,
3676
+ baseline: (projectType?.baseline ?? []).map(detection),
3677
+ optionalCapabilities: (projectType?.optionalCapabilities ?? []).map((id) => ({ ...detection(id), selected: selected.has(id) })),
3678
+ exceptions: endpoint.technologyExceptions ?? []
3679
+ };
3680
+ }
3681
+ function hasBaselineException(projectModel, technologyId) {
3682
+ return projectModel.exceptions.some(
3683
+ (exception) => exception.technology === technologyId && exception.classification !== "urgent-drift"
3684
+ );
3685
+ }
3686
+ function collectPackageNames(root) {
3687
+ const names = /* @__PURE__ */ new Set();
3688
+ let files = [];
3689
+ try {
3690
+ files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
3691
+ } catch {
3692
+ if (existsSync21(join20(root, "package.json"))) files = ["package.json"];
3693
+ }
3694
+ for (const file of files) {
3695
+ const packageJson = readJson3(join20(root, file));
3696
+ if (!isRecord3(packageJson)) continue;
3697
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3698
+ const dependencies = packageJson[section];
3699
+ if (!isRecord3(dependencies)) continue;
3700
+ for (const name of Object.keys(dependencies)) names.add(name);
3701
+ }
3702
+ }
3703
+ return names;
3704
+ }
3504
3705
  function hasWorkflowReminderHooks(hooks) {
3505
3706
  return hooks.some((hook) => hook.events.some((event) => event.name === "Stop" || event.name === "SubagentStop"));
3506
3707
  }
@@ -3617,11 +3818,14 @@ function inspectClaudeSkillRoot(root) {
3617
3818
  }
3618
3819
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository) {
3619
3820
  const centralPath = join20(secretsRoot, id);
3821
+ const centralRealPath = safeRealpath(centralPath);
3620
3822
  const envFiles = collectEnvironmentFiles(root).map((path) => ({
3621
3823
  path,
3622
3824
  tracked: isRepository ? gitTracks(root, path) : false,
3623
- template: /(?:example|sample|template|defaults?)$/i.test(basenameOnly(path)),
3624
- symlink: lstatSync6(join20(root, path)).isSymbolicLink()
3825
+ template: isEnvironmentTemplate(path),
3826
+ fixture: isEnvironmentFixture(path),
3827
+ symlink: lstatSync6(join20(root, path)).isSymbolicLink(),
3828
+ centralized: pointsInside(join20(root, path), centralRealPath)
3625
3829
  }));
3626
3830
  return {
3627
3831
  centralDirectory: existsSync21(centralPath) ? "present" : "absent",
@@ -3630,7 +3834,7 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository) {
3630
3834
  repositoryEnvFiles: envFiles
3631
3835
  };
3632
3836
  }
3633
- var SKIP_ENV_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".next", ".nuxt", ".output", ".turbo", ".worktrees", "build", "coverage", "dist", "node_modules", "out", "target"]);
3837
+ var SKIP_ENV_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".next", ".nuxt", ".output", ".turbo", ".vercel", ".worktrees", "build", "coverage", "dist", "node_modules", "out", "target"]);
3634
3838
  function collectEnvironmentFiles(root, current = root, depth = 0) {
3635
3839
  if (depth > 5) return [];
3636
3840
  const found = [];
@@ -3638,7 +3842,7 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
3638
3842
  for (const entry of readdirSync8(current, { withFileTypes: true })) {
3639
3843
  if (entry.isDirectory()) {
3640
3844
  if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join20(current, entry.name), depth + 1));
3641
- } else if (entry.name === ".env" || entry.name.startsWith(".env.")) {
3845
+ } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
3642
3846
  found.push(relative8(root, join20(current, entry.name)));
3643
3847
  }
3644
3848
  }
@@ -3665,6 +3869,35 @@ function basenameOnly(path) {
3665
3869
  const parts = path.split(/[\\/]/);
3666
3870
  return parts[parts.length - 1] ?? path;
3667
3871
  }
3872
+ function isEnvironmentFilename(name) {
3873
+ return name === ".env" || name.startsWith(".env.") || name.endsWith(".env");
3874
+ }
3875
+ function isProviderGeneratedEnvironmentFile(name) {
3876
+ return name.endsWith(".pulled");
3877
+ }
3878
+ function isEnvironmentTemplate(path) {
3879
+ return /(^|[._-])(example|sample|template|defaults?)([._-]|$)/i.test(basenameOnly(path));
3880
+ }
3881
+ function isEnvironmentFixture(path) {
3882
+ return /(^|[\\/])(?:tests?|__tests__)[\\/]fixtures?[\\/]/i.test(path) || /(^|[\\/])__fixtures__[\\/]/i.test(path);
3883
+ }
3884
+ function safeRealpath(path) {
3885
+ try {
3886
+ return realpathSync2(path);
3887
+ } catch {
3888
+ return void 0;
3889
+ }
3890
+ }
3891
+ function pointsInside(path, expectedRoot) {
3892
+ if (!expectedRoot || !lstatSync6(path).isSymbolicLink()) return false;
3893
+ const target = safeRealpath(path);
3894
+ if (!target) return false;
3895
+ const fromRoot = relative8(expectedRoot, target);
3896
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
3897
+ }
3898
+ function hasUnsafeCentralSecretPermissions(secrets) {
3899
+ return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
3900
+ }
3668
3901
  function inspectSecretsRoot(path) {
3669
3902
  return existsSync21(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
3670
3903
  }
@@ -4293,7 +4526,7 @@ function normalizeMarkdownTableCell(cell) {
4293
4526
  }
4294
4527
  function inferInstalledProfile(root) {
4295
4528
  const installed = ["engineering-runtime", "doc-only"].filter(
4296
- (profile) => existsSync23(join22(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
4529
+ (profile) => existsSync23(join22(root, `docs/governance/agents-routing/${profile}-v1.0.md`))
4297
4530
  );
4298
4531
  return installed.length === 1 ? installed[0] : void 0;
4299
4532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/pro-gov",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "Project-level distribution kit for Project Governance System.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@pieai/doc-gov": "^0.4.8"
38
+ "@pieai/doc-gov": "^0.5.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@pieai/swimmer-ui-kit": "1.0.1",