hive-lite 0.1.6 → 0.1.7

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/README.md CHANGED
@@ -74,9 +74,9 @@ node bin/hive.js next --agent codex --json
74
74
  `status` and `next` show actionable split notes by default: active split notes with accepted phase progress, or the newest unstarted split note when no active split exists. Use `status --all` to inspect every runtime split note. `next` may recommend start, finish, map maintenance, split continuation, dirty-worktree cleanup, or git repo setup. It does not run agents, commit, accept risk, or modify files.
75
75
  In JSON output, `splitNoteSummary.primaryRecentSplitNote` identifies the split note that `status`/`next` treat as primary, and `splitNoteSummary.otherActiveSplitNotes` lists older active split notes with accepted progress.
76
76
 
77
- When you pass `next --agent <codex|claude|gemini>` or `next --path <skills-dir>`, `next` also checks whether the recommended Hive Lite operator skill is installed in that target. If it is missing or stale, `next` recommends installing/syncing skills before the handoff.
77
+ When you pass `next --agent <codex|claude|gemini>` or `next --path <skills-dir>`, `next` also checks whether the recommended Hive Lite operator skill is current in that target. If it is missing or stale, `next` recommends `skills sync` before the handoff.
78
78
 
79
- ### Install Agent Skills
79
+ ### Sync Agent Skills
80
80
 
81
81
  Hive Lite ships four operator skills:
82
82
 
@@ -98,22 +98,22 @@ node bin/hive.js skills doctor --json
98
98
 
99
99
  Codex, Claude, and Gemini targets are global user-skill installs. `--agent codex` writes to `~/.codex/skills`, `--agent claude` writes to `~/.claude/skills`, and `--agent gemini` writes to `~/.gemini/skills`. They do not create or update repo-local `.codex/skills`, `.claude/skills`, or `.gemini/skills`. Use `--path <repo-local-skills-dir>` only when you intentionally want a custom repo-local copy that Hive Lite can inspect.
100
100
 
101
- Install missing skills:
101
+ Install or update skills in one step:
102
102
 
103
103
  ```bash
104
- node bin/hive.js skills install --agent codex
105
- node bin/hive.js skills install --agent claude
106
- node bin/hive.js skills install --agent gemini
107
- node bin/hive.js skills install --agent all
104
+ node bin/hive.js skills sync --agent codex
105
+ node bin/hive.js skills sync --agent claude
106
+ node bin/hive.js skills sync --agent gemini
107
+ node bin/hive.js skills sync --agent all
108
108
  ```
109
109
 
110
- `install` creates missing skills but does not overwrite stale local copies unless `--force` is passed.
110
+ `sync` installs missing skills and overwrites stale copies with the bundled version.
111
111
 
112
- Synchronize bundled skills over existing stale copies:
112
+ `install` is still available for conservative installs that should not overwrite stale local copies unless `--force` is passed:
113
113
 
114
114
  ```bash
115
- node bin/hive.js skills sync --agent codex
116
- node bin/hive.js skills sync --agent all
115
+ node bin/hive.js skills install --agent codex
116
+ node bin/hive.js skills install --agent all
117
117
  ```
118
118
 
119
119
  Use `--dry-run` to preview writes. Use `--path <skills-dir>` for a custom target.
@@ -56,7 +56,7 @@ Use when a real requirement hits a map gap during start:
56
56
 
57
57
  - `find.mode` is `needs_map` or `discovery_context`.
58
58
  - `find` selected a generic/broad area.
59
- - warnings include `NO_CONFIDENT_AREA`, `MISSING_DIRECT_WRITABLE_SCOPE`, `MISSING_ENTRYPOINT`, or `MISSING_VALIDATION`.
59
+ - warnings include `NO_CONFIDENT_AREA`, `MISSING_DIRECT_WRITABLE_SCOPE`, `MISSING_DIRECT_NEW_FILE_SCOPE`, `DIRECT_ONLY_REFERENCE_PEERS`, `BROAD_FALLBACK_ONLY`, `TARGET_ENTITY_MISMATCH`, `MISSING_ARTIFACT_FAMILY_SCOPE`, `MISSING_REQUIRED_HOOK_SCOPE`, `MISSING_ENTRYPOINT`, or `MISSING_VALIDATION`.
60
60
  - `map health --area <selected>` shows critical findings.
61
61
 
62
62
  Goal:
@@ -55,6 +55,10 @@ Rules:
55
55
 
56
56
  - `readable` may be broad.
57
57
  - `writable_direct` should be exact existing files, usually 2-8 files.
58
+ - Prefer `writable_existing` for exact update targets and `writable_create_patterns` for new file permissions when the repo has repeated artifact families.
59
+ - For intents that create new peer files, use very small direct patterns that match only the expected family, such as `src/providers/*Provider.ts`; do not fall back to an entire source tree.
60
+ - Existing peer files that are only copy/reference examples should stay readable/reference context, not selected Direct Writable for a new peer target.
61
+ - Put peer examples under `readable_reference` and describe repeated families under `artifact_families` when possible.
58
62
  - Broad patterns such as `apps/*/src/**`, `packages/**`, `server/**`, and `**` must not be in `writable_direct`.
59
63
  - Every conditional/fallback item needs `reason` and `requires_review: true`.
60
64
 
@@ -180,8 +180,9 @@ Accept the packet only when all are true:
180
180
  - `mode` is `edit_context`.
181
181
  - `area.id` is present.
182
182
  - `writableScope` contains at least one narrow direct file.
183
+ - `writePlan.blockingWarnings` is empty when present.
183
184
  - `validationPlan` is not empty.
184
- - `warnings` has no map-gap warning such as `NO_CONFIDENT_AREA`, `MISSING_DIRECT_WRITABLE_SCOPE`, `MISSING_ENTRYPOINT`, or `MISSING_VALIDATION`.
185
+ - `warnings` has no map-gap warning such as `NO_CONFIDENT_AREA`, `MISSING_DIRECT_WRITABLE_SCOPE`, `MISSING_DIRECT_NEW_FILE_SCOPE`, `DIRECT_ONLY_REFERENCE_PEERS`, `BROAD_FALLBACK_ONLY`, `TARGET_ENTITY_MISMATCH`, `MISSING_ENTRYPOINT`, or `MISSING_VALIDATION`.
185
186
  - `hive-lite map health --area <areaId> --json` reports no critical findings.
186
187
 
187
188
  Review-gated conditional or broad fallback scope is allowed. Include it in the final prompt as a boundary, not as a failure.
@@ -200,7 +201,12 @@ Repair principles:
200
201
 
201
202
  - Prefer focused product/work area ids such as `dashboard.action_inbox`, not one-part ids such as `dashboard`.
202
203
  - Preserve unrelated areas exactly unless a change is needed for the reported map gap.
203
- - Use `scope.writable_direct` for exact files the coding agent may edit.
204
+ - Use `scope.writable_direct` for exact files the coding agent may edit, or very small direct patterns when the intent needs new peer files such as a provider, proxy, adapter, connector, or plugin.
205
+ - Prefer `scope.writable_existing` for exact update targets and `scope.writable_create_patterns` for narrow create permissions when the map needs operation-specific write capabilities.
206
+ - Put copy sources and peer examples under `scope.readable_reference` when they should guide implementation but not be edited.
207
+ - Use `artifact_families` to declare required create artifacts and hook files for repeated artifact families such as providers, adapters, connectors, endpoints, DTOs, tests, or migrations.
208
+ - Treat Direct Writable as the selected intent-specific write target list, not the area's full writable maximum.
209
+ - Keep peer examples, entrypoints, and similar existing files as reference context unless the current intent specifically requires editing them.
204
210
  - Put conditional files under `scope.writable_conditional` with `reason` and `requires_review: true`.
205
211
  - Put broad patterns only under `scope.writable_broad_fallback` with `reason` and `requires_review: true`.
206
212
  - Never put broad patterns such as `apps/*/src/**`, `packages/**`, `server/**`, or `**` in `writable_direct`.
@@ -270,6 +276,9 @@ Use this context packet as your source of truth. Edit only Direct Writable files
270
276
  Direct Writable:
271
277
  - <file>
272
278
 
279
+ Reference Files:
280
+ - <file and reason, only when useful; do not edit unless it is also Direct Writable>
281
+
273
282
  Review-Gated:
274
283
  - <conditional/broad item and reason>
275
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-lite",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Local Project Map + Change Control for coding agents.",
5
5
  "keywords": [
6
6
  "codex",
@@ -37,4 +37,4 @@
37
37
  "engines": {
38
38
  "node": ">=16"
39
39
  }
40
- }
40
+ }
package/src/cli.js CHANGED
@@ -230,6 +230,22 @@ function printContext(result) {
230
230
  console.log(` risk: ${item.risk}`);
231
231
  }
232
232
  }
233
+ if (packet.writePlan && packet.writePlan.hypotheses && packet.writePlan.hypotheses.length > 0) {
234
+ console.log('');
235
+ console.log('Write Plan:');
236
+ for (const item of packet.writePlan.hypotheses) {
237
+ console.log(` - ${item.action} ${item.artifactFamily}: ${item.operation}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''}`);
238
+ }
239
+ if (packet.writePlan.blockingWarnings && packet.writePlan.blockingWarnings.length > 0) {
240
+ console.log(' blocking:');
241
+ for (const item of packet.writePlan.blockingWarnings) console.log(` - ${item.code}: ${item.message}`);
242
+ }
243
+ }
244
+ if (packet.writePlan && packet.writePlan.referenceFiles && packet.writePlan.referenceFiles.length > 0) {
245
+ console.log('');
246
+ console.log('Reference Files:');
247
+ for (const file of packet.writePlan.referenceFiles) console.log(` - ${file.path}\n reason: ${file.reason}`);
248
+ }
233
249
  console.log('');
234
250
  console.log('Relevant Files:');
235
251
  if (packet.relevantFiles.length === 0) console.log(' - (none found)');
@@ -242,6 +258,14 @@ function printContext(result) {
242
258
  console.log('');
243
259
  console.log(`Scope Quality: ${packet.scope.quality}`);
244
260
  console.log('');
261
+ console.log('Writable Existing Scope:');
262
+ if (!packet.scope.writableExisting || packet.scope.writableExisting.length === 0) console.log(' - (none configured)');
263
+ for (const item of packet.scope.writableExisting || []) console.log(` - ${patternDisplay(item)}`);
264
+ console.log('');
265
+ console.log('Writable Create Patterns:');
266
+ if (!packet.scope.writableCreatePatterns || packet.scope.writableCreatePatterns.length === 0) console.log(' - (none configured)');
267
+ for (const item of packet.scope.writableCreatePatterns || []) console.log(` - ${patternDisplay(item)}`);
268
+ console.log('');
245
269
  console.log('Conditional Writable Scope:');
246
270
  if (packet.scope.writableConditional.length === 0) console.log(' - (none configured)');
247
271
  for (const item of packet.scope.writableConditional) console.log(` - ${patternDisplay(item)}`);
@@ -363,6 +387,24 @@ function printCheck(root, result) {
363
387
  console.log(' broad fallback:');
364
388
  for (const item of change.scope.writableBroadFallback) console.log(` - ${patternDisplay(item)}`);
365
389
  }
390
+ if (change.writePlanStatus) {
391
+ console.log('');
392
+ console.log('Write Plan:');
393
+ console.log(` ${change.writePlanStatus.status}`);
394
+ if (change.writePlanStatus.contextMode) console.log(` context mode: ${change.writePlanStatus.contextMode}`);
395
+ if (change.writePlanStatus.selectedWritableDirect && change.writePlanStatus.selectedWritableDirect.length > 0) {
396
+ console.log(' selected direct:');
397
+ for (const item of change.writePlanStatus.selectedWritableDirect) console.log(` - ${item}`);
398
+ }
399
+ if (change.writePlanStatus.referenceOnlyTouched && change.writePlanStatus.referenceOnlyTouched.length > 0) {
400
+ console.log(' reference-only touched:');
401
+ for (const item of change.writePlanStatus.referenceOnlyTouched) console.log(` - ${item.path}: ${item.reason || item.reference}`);
402
+ }
403
+ if (change.writePlanStatus.blockingReasons && change.writePlanStatus.blockingReasons.length > 0) {
404
+ console.log(' blockers:');
405
+ for (const reason of change.writePlanStatus.blockingReasons) console.log(` - ${reason}`);
406
+ }
407
+ }
366
408
  console.log('');
367
409
  console.log('Validation:');
368
410
  console.log(` ${change.validation.status}`);
package/src/lib/change.js CHANGED
@@ -174,9 +174,15 @@ function scopeFromContext(root, context) {
174
174
  });
175
175
  }
176
176
  const scope = context.scope;
177
+ const selectedDirect = Array.isArray(context.writableScope) && context.writableScope.length > 0
178
+ ? context.writableScope
179
+ : (scope.writableDirect || []);
177
180
  return {
178
181
  readable: (scope.readable || []).slice(),
179
- writableDirect: (scope.writableDirect || []).map(itemPattern).filter(Boolean),
182
+ readableReference: (scope.readableReference || []).slice(),
183
+ writableDirect: selectedDirect.map(itemPattern).filter(Boolean),
184
+ writableExisting: normalizePatternList(scope.writableExisting || [], { source: 'writable_existing' }),
185
+ writableCreatePatterns: normalizePatternList(scope.writableCreatePatterns || [], { source: 'writable_create_patterns' }),
180
186
  writableConditional: normalizePatternList(scope.writableConditional || [], { requiresReview: true }),
181
187
  writableBroadFallback: normalizePatternList(scope.writableBroadFallback || [], { requiresReview: true }),
182
188
  forbidden: (scope.forbidden || []).slice(),
@@ -190,6 +196,9 @@ function firstItemMatch(file, items) {
190
196
 
191
197
  function scopeCheck(root, files, context, map) {
192
198
  const filePaths = files.map((item) => item.path || item);
199
+ const referenceOnly = context && context.writePlan && Array.isArray(context.writePlan.referenceFiles)
200
+ ? context.writePlan.referenceFiles.map((item) => item.path || item.pattern).filter(Boolean)
201
+ : [];
193
202
  const matchedAreas = [];
194
203
  let scope = {
195
204
  readable: [],
@@ -236,6 +245,13 @@ function scopeCheck(root, files, context, map) {
236
245
  violations.push(`${file} matched doNotTouch ${forbidden}`);
237
246
  continue;
238
247
  }
248
+ const reference = firstPatternMatch(file, referenceOnly);
249
+ if (reference) {
250
+ matchedTiers.unmatched.push(file);
251
+ review.push(file);
252
+ reviewDetails.push(`${file} matched reference-only file ${reference}`);
253
+ continue;
254
+ }
239
255
  const direct = firstItemMatch(file, scope.writableDirect);
240
256
  if (direct) {
241
257
  matchedTiers.direct.push(file);
@@ -298,6 +314,63 @@ function validationPlanFromContextOrMap(context, map) {
298
314
  }] : [];
299
315
  }
300
316
 
317
+ function evaluateWritePlanStatus(files, context) {
318
+ if (!context) {
319
+ return {
320
+ status: 'not_available',
321
+ contextMode: null,
322
+ blockingReasons: [],
323
+ selectedWritableDirect: [],
324
+ referenceOnlyTouched: [],
325
+ requiredWrites: [],
326
+ blockingWarnings: [],
327
+ };
328
+ }
329
+
330
+ const writePlan = context.writePlan || null;
331
+ const blockingWarnings = writePlan && Array.isArray(writePlan.blockingWarnings)
332
+ ? writePlan.blockingWarnings
333
+ : [];
334
+ const selectedWritableDirect = Array.isArray(context.writableScope) ? context.writableScope : [];
335
+ const changedPaths = files.map((item) => item.path || item);
336
+ const referenceFiles = writePlan && Array.isArray(writePlan.referenceFiles) ? writePlan.referenceFiles : [];
337
+ const referenceOnlyTouched = [];
338
+ for (const file of changedPaths) {
339
+ const reference = referenceFiles.find((item) => matchesPattern(file, item.path || item.pattern));
340
+ if (reference) {
341
+ referenceOnlyTouched.push({
342
+ path: file,
343
+ reference: reference.path || reference.pattern,
344
+ reason: reference.reason || '',
345
+ });
346
+ }
347
+ }
348
+
349
+ const blockingReasons = [];
350
+ if (context.mode && context.mode !== 'edit_context') {
351
+ blockingReasons.push(`context mode is ${context.mode}, not edit_context`);
352
+ }
353
+ for (const warning of blockingWarnings) {
354
+ blockingReasons.push(`${warning.code}: ${warning.message}`);
355
+ }
356
+ for (const item of referenceOnlyTouched) {
357
+ blockingReasons.push(`${item.path} touched reference-only file ${item.reference}`);
358
+ }
359
+ if (selectedWritableDirect.length === 0 && changedPaths.length > 0) {
360
+ blockingReasons.push('context has no selected Direct Writable scope');
361
+ }
362
+
363
+ return {
364
+ status: blockingReasons.length > 0 ? 'blocked' : 'clean',
365
+ contextMode: context.mode || null,
366
+ blockingReasons: [...new Set(blockingReasons)],
367
+ selectedWritableDirect,
368
+ referenceOnlyTouched,
369
+ requiredWrites: writePlan && Array.isArray(writePlan.hypotheses) ? writePlan.hypotheses : [],
370
+ blockingWarnings,
371
+ };
372
+ }
373
+
301
374
  function createOrUpdateChange(cwd, options = {}) {
302
375
  const root = repoRoot(cwd);
303
376
  const map = loadProjectMap(root);
@@ -339,6 +412,7 @@ function createOrUpdateChange(cwd, options = {}) {
339
412
  text: diffText,
340
413
  },
341
414
  scope: scopeCheck(root, files, context, map),
415
+ writePlanStatus: evaluateWritePlanStatus(files, context),
342
416
  validation: {
343
417
  status: existing && existing.validation ? existing.validation.status : 'not_run',
344
418
  plan: validationPlanFromContextOrMap(context, map),
@@ -40,6 +40,374 @@ function tokenize(value) {
40
40
  return [...new Set([...latin, ...cjk])];
41
41
  }
42
42
 
43
+ function unique(values) {
44
+ return [...new Set((values || []).filter(Boolean))];
45
+ }
46
+
47
+ function looksLikeFilePattern(pattern) {
48
+ if (String(pattern || '').includes('*')) return false;
49
+ const base = path.basename(String(pattern || ''));
50
+ return /\.[A-Za-z0-9]+$/.test(base);
51
+ }
52
+
53
+ function isCreateCapablePattern(pattern) {
54
+ return !looksLikeFilePattern(pattern);
55
+ }
56
+
57
+ const IDENTITY_ALIASES = {
58
+ openai: ['openai', 'chatgpt', 'gpt'],
59
+ google: ['google', 'gemini'],
60
+ qwen: ['qwen', 'tongyi'],
61
+ wan: ['wan'],
62
+ aliyun: ['aliyun', 'ali'],
63
+ anthropic: ['anthropic', 'claude'],
64
+ azure: ['azure'],
65
+ };
66
+
67
+ function canonicalIdentity(value) {
68
+ const text = normalize(value);
69
+ if (!text) return null;
70
+ for (const [canonical, aliases] of Object.entries(IDENTITY_ALIASES)) {
71
+ if (aliases.includes(text)) return canonical;
72
+ }
73
+ return text;
74
+ }
75
+
76
+ function targetIdentityForIntent(intent) {
77
+ const tokens = tokenize(intent);
78
+ for (const token of tokens) {
79
+ const identity = canonicalIdentity(token);
80
+ if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function peerIdentityFromPath(pattern) {
86
+ if (!looksLikeFilePattern(pattern)) return null;
87
+ const tokens = basenameTokens(pattern);
88
+ for (const token of tokens) {
89
+ const identity = canonicalIdentity(token);
90
+ if (identity && Object.prototype.hasOwnProperty.call(IDENTITY_ALIASES, identity)) return identity;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function identityMatches(left, right) {
96
+ if (!left || !right) return false;
97
+ return canonicalIdentity(left) === canonicalIdentity(right);
98
+ }
99
+
100
+ function intentTargetsAllPeers(intent) {
101
+ const text = normalize(intent);
102
+ return [
103
+ /\b(all|every|each)\b/,
104
+ /所有|全部|每个|统一|全量|全部/,
105
+ ].some((pattern) => pattern.test(text));
106
+ }
107
+
108
+ function arrayFrom(value) {
109
+ if (Array.isArray(value)) return value.map((item) => String(item || '')).filter(Boolean);
110
+ if (value == null || value === '') return [];
111
+ if (typeof value === 'object') return [];
112
+ return [String(value)];
113
+ }
114
+
115
+ function configuredArtifactFamilies(area) {
116
+ const raw = area && area.artifact_families ? area.artifact_families : null;
117
+ if (!raw) return [];
118
+ if (Array.isArray(raw)) {
119
+ return raw.map((item) => ({
120
+ id: item.id || item.name || '',
121
+ triggerTerms: [
122
+ ...arrayFrom(item.trigger_terms || item.triggerTerms),
123
+ ...arrayFrom(item.trigger_terms && item.trigger_terms.en),
124
+ ...arrayFrom(item.trigger_terms && item.trigger_terms.zh),
125
+ ],
126
+ createRequires: arrayFrom(item.create_requires || item.createRequires),
127
+ hooks: arrayFrom(item.hooks),
128
+ examples: arrayFrom(item.examples),
129
+ })).filter((item) => item.id);
130
+ }
131
+ return Object.entries(raw).map(([id, value]) => ({
132
+ id,
133
+ triggerTerms: [
134
+ ...arrayFrom(value && (value.trigger_terms || value.triggerTerms)),
135
+ ...arrayFrom(value && value.trigger_terms && value.trigger_terms.en),
136
+ ...arrayFrom(value && value.trigger_terms && value.trigger_terms.zh),
137
+ ],
138
+ createRequires: arrayFrom(value && (value.create_requires || value.createRequires)),
139
+ hooks: arrayFrom(value && value.hooks),
140
+ examples: arrayFrom(value && value.examples),
141
+ }));
142
+ }
143
+
144
+ function configuredArtifactFamilyForIntent(area, intent) {
145
+ const text = normalize(intent);
146
+ for (const family of configuredArtifactFamilies(area)) {
147
+ const terms = unique([
148
+ family.id,
149
+ ...family.triggerTerms,
150
+ ]).map(normalize).filter(Boolean);
151
+ if (terms.some((term) => text.includes(term))) return family;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function classifyArtifactFamily(intent, area) {
157
+ const configured = configuredArtifactFamilyForIntent(area, intent);
158
+ if (configured) return configured.id;
159
+ const text = normalize(intent);
160
+ if ([
161
+ /\b(provider|proxy|adapter|integration|connector|plugin)\b/,
162
+ /代理|提供商|适配器|集成|连接器|插件/,
163
+ ].some((pattern) => pattern.test(text))) return 'provider_proxy';
164
+ if ([
165
+ /\b(endpoint|controller|route|api)\b/,
166
+ /接口|端点|路由|控制器/,
167
+ ].some((pattern) => pattern.test(text))) return 'endpoint';
168
+ if ([
169
+ /\b(dto|request|response|payload)\b/,
170
+ /请求|响应|入参|出参/,
171
+ ].some((pattern) => pattern.test(text))) return 'dto';
172
+ if ([
173
+ /\b(migration|database|db|sql)\b/,
174
+ /数据库|迁移|表结构/,
175
+ ].some((pattern) => pattern.test(text))) return 'migration';
176
+ if ([
177
+ /\b(test|spec)\b/,
178
+ /测试|用例/,
179
+ ].some((pattern) => pattern.test(text))) return 'test';
180
+ if ([
181
+ /\b(component|page|view|ui)\b/,
182
+ /组件|页面|视图|界面/,
183
+ ].some((pattern) => pattern.test(text))) return 'component';
184
+ if ([
185
+ /\b(service)\b/,
186
+ /服务/,
187
+ ].some((pattern) => pattern.test(text))) return 'service';
188
+ return 'unknown';
189
+ }
190
+
191
+ function classifyIntentWrite(intent, area) {
192
+ const text = normalize(intent);
193
+ const createTerm = [
194
+ /\b(add|create|new|introduce|support|implement)\b/,
195
+ /新增|新建|创建|添加|接入|支持/,
196
+ ].some((pattern) => pattern.test(text));
197
+ const deleteTerm = [
198
+ /\b(delete|remove)\b/,
199
+ /删除|移除/,
200
+ ].some((pattern) => pattern.test(text));
201
+ const renameTerm = [
202
+ /\b(rename|move)\b/,
203
+ /重命名|改名|迁移/,
204
+ ].some((pattern) => pattern.test(text));
205
+ const targetAll = intentTargetsAllPeers(intent);
206
+ const artifactFamily = classifyArtifactFamily(intent, area);
207
+ const familyConfig = configuredArtifactFamilies(area).find((item) => item.id === artifactFamily) || null;
208
+ const targetIdentity = targetIdentityForIntent(intent);
209
+ let action = 'modify';
210
+ if (deleteTerm) action = 'delete';
211
+ else if (renameTerm) action = 'rename';
212
+ else if (createTerm && !targetAll) action = 'create';
213
+ return {
214
+ action,
215
+ artifactFamily,
216
+ targetIdentity,
217
+ operation: action === 'create' && artifactFamily === 'provider_proxy' ? 'create_file' : 'modify_existing_file',
218
+ required: true,
219
+ targetAll,
220
+ intentKind: action === 'create' ? `add_${artifactFamily}` : `${action}_${artifactFamily}`,
221
+ createRequires: familyConfig ? familyConfig.createRequires : [],
222
+ hooks: familyConfig ? familyConfig.hooks : [],
223
+ confidence: artifactFamily === 'unknown' ? 'low' : 'medium',
224
+ };
225
+ }
226
+
227
+ function directCapabilities(scope) {
228
+ const explicitItems = [
229
+ ...(scope.writableDirectItems || []),
230
+ ];
231
+ const items = explicitItems.length
232
+ ? explicitItems
233
+ : (scope.writableDirect || []).map((pattern) => ({ pattern, source: 'writable_direct' }));
234
+ const seen = new Set();
235
+ return items.map((item) => {
236
+ const pattern = item.pattern || item.path || item;
237
+ const createCapable = isCreateCapablePattern(pattern);
238
+ const configuredOperations = arrayFrom(item.operations);
239
+ const operations = configuredOperations.length
240
+ ? configuredOperations
241
+ : (createCapable ? ['update_existing', 'create_file'] : ['update_existing']);
242
+ const key = `${pattern}:${operations.join(',')}:${item.source || ''}:${item.artifact || ''}`;
243
+ if (seen.has(key)) return null;
244
+ seen.add(key);
245
+ return {
246
+ scope: pattern,
247
+ operations,
248
+ breadth: createCapable ? 'narrow_pattern' : 'exact',
249
+ reviewGated: false,
250
+ source: item.source || 'writable_direct',
251
+ artifact: item.artifact || null,
252
+ artifactFamily: item.artifactFamily || item.artifact_family || null,
253
+ intentKinds: arrayFrom(item.intentKinds || item.intent_kinds),
254
+ requiredWhen: arrayFrom(item.requiredWhen || item.required_when),
255
+ targetSlot: item.targetSlot || item.target_slot || null,
256
+ peerIdentity: canonicalIdentity(item.targetIdentity || item.target_identity || item.peerIdentity || item.peer_identity) || peerIdentityFromPath(pattern),
257
+ };
258
+ }).filter(Boolean);
259
+ }
260
+
261
+ function warning(code, message, data = {}) {
262
+ return {
263
+ code,
264
+ message,
265
+ blocking: data.blocking !== false,
266
+ ...data,
267
+ };
268
+ }
269
+
270
+ function buildReference(capability, reason) {
271
+ return {
272
+ path: capability.scope || capability.path,
273
+ role: capability.role || 'peer_example',
274
+ writable: false,
275
+ reason,
276
+ artifact: capability.artifact || null,
277
+ peerIdentity: capability.peerIdentity || null,
278
+ };
279
+ }
280
+
281
+ function artifactMatches(capability, artifact) {
282
+ if (!artifact) return false;
283
+ return capability.artifact === artifact || capability.artifactFamily === artifact;
284
+ }
285
+
286
+ function capabilityMatchesIntent(capability, hypothesis) {
287
+ if (!capability.intentKinds || capability.intentKinds.length === 0) return true;
288
+ return capability.intentKinds.includes(hypothesis.intentKind);
289
+ }
290
+
291
+ function buildWritePlan(scope, intent, area) {
292
+ const hypothesis = classifyIntentWrite(intent, area);
293
+ const capabilities = directCapabilities(scope);
294
+ const warnings = [];
295
+ const references = (scope.readableReference || []).map((item) => buildReference(
296
+ {
297
+ path: item.path,
298
+ role: item.role || 'reference',
299
+ artifact: item.artifact || null,
300
+ peerIdentity: canonicalIdentity(item.peerIdentity) || peerIdentityFromPath(item.path),
301
+ },
302
+ item.reason || 'readable reference from Project Map'
303
+ ));
304
+ let selected = capabilities.map((item) => item.scope);
305
+
306
+ if (hypothesis.operation === 'create_file' && hypothesis.artifactFamily === 'provider_proxy') {
307
+ const createCapabilities = capabilities.filter((item) => (
308
+ item.operations.includes('create_file') && capabilityMatchesIntent(item, hypothesis)
309
+ ));
310
+ const genericExistingCapabilities = capabilities.filter((item) => (
311
+ !item.peerIdentity && !item.operations.includes('create_file')
312
+ ));
313
+ const referencePeers = capabilities.filter((item) => (
314
+ item.peerIdentity
315
+ && hypothesis.targetIdentity
316
+ && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
317
+ ));
318
+ const selectedCreateCapabilities = hypothesis.createRequires.length
319
+ ? createCapabilities.filter((item) => hypothesis.createRequires.some((artifact) => artifactMatches(item, artifact)))
320
+ : createCapabilities;
321
+ const hookCapabilities = hypothesis.hooks.length
322
+ ? genericExistingCapabilities.filter((item) => hypothesis.hooks.some((artifact) => artifactMatches(item, artifact)))
323
+ : genericExistingCapabilities;
324
+
325
+ selected = selectedCreateCapabilities.length > 0
326
+ ? [...selectedCreateCapabilities, ...hookCapabilities].map((item) => item.scope)
327
+ : [];
328
+ for (const capability of referencePeers) {
329
+ references.push(buildReference(
330
+ capability,
331
+ `peer identity ${capability.peerIdentity} differs from target ${hypothesis.targetIdentity}; use as a reference example only`
332
+ ));
333
+ }
334
+
335
+ if (createCapabilities.length === 0) {
336
+ warnings.push(warning(
337
+ 'MISSING_DIRECT_NEW_FILE_SCOPE',
338
+ 'Intent appears to add a new peer provider/proxy artifact, but direct writable scope does not include a narrow create pattern.'
339
+ ));
340
+ }
341
+ for (const artifact of hypothesis.createRequires) {
342
+ if (!createCapabilities.some((item) => artifactMatches(item, artifact))) {
343
+ warnings.push(warning(
344
+ 'MISSING_ARTIFACT_FAMILY_SCOPE',
345
+ `Intent requires provider/proxy artifact ${artifact}, but no direct create capability covers it.`,
346
+ { artifact }
347
+ ));
348
+ }
349
+ }
350
+ for (const artifact of hypothesis.hooks) {
351
+ if (!genericExistingCapabilities.some((item) => artifactMatches(item, artifact))) {
352
+ warnings.push(warning(
353
+ 'MISSING_REQUIRED_HOOK_SCOPE',
354
+ `Intent requires provider/proxy hook ${artifact}, but no direct existing-file capability covers it.`,
355
+ { artifact }
356
+ ));
357
+ }
358
+ }
359
+ if (referencePeers.length > 0 && createCapabilities.length === 0) {
360
+ warnings.push(warning(
361
+ 'DIRECT_ONLY_REFERENCE_PEERS',
362
+ 'Direct writable scope only contains existing peer provider/proxy files for other identities; those are reference examples, not the target write files.'
363
+ ));
364
+ }
365
+ if (createCapabilities.length === 0 && (scope.writableBroadFallback || []).length > 0) {
366
+ warnings.push(warning(
367
+ 'BROAD_FALLBACK_ONLY',
368
+ 'Only broad fallback scope appears able to cover new provider/proxy files; broad fallback is review-gated and is not direct write permission.'
369
+ ));
370
+ }
371
+ } else if (
372
+ hypothesis.artifactFamily === 'provider_proxy'
373
+ && hypothesis.targetIdentity
374
+ && !hypothesis.targetAll
375
+ ) {
376
+ const targetMatches = capabilities.filter((item) => (
377
+ item.peerIdentity && identityMatches(item.peerIdentity, hypothesis.targetIdentity)
378
+ ));
379
+ const genericCapabilities = capabilities.filter((item) => (
380
+ !item.peerIdentity && !item.operations.includes('create_file')
381
+ ));
382
+ const referencePeers = capabilities.filter((item) => (
383
+ item.peerIdentity && !identityMatches(item.peerIdentity, hypothesis.targetIdentity)
384
+ ));
385
+ selected = unique([...targetMatches, ...genericCapabilities].map((item) => item.scope));
386
+ for (const capability of referencePeers) {
387
+ references.push(buildReference(
388
+ capability,
389
+ `peer identity ${capability.peerIdentity} differs from target ${hypothesis.targetIdentity}`
390
+ ));
391
+ }
392
+ if (targetMatches.length === 0 && referencePeers.length > 0) {
393
+ warnings.push(warning(
394
+ 'TARGET_ENTITY_MISMATCH',
395
+ `Intent targets ${hypothesis.targetIdentity}, but direct writable peer files are for other identities.`
396
+ ));
397
+ }
398
+ }
399
+
400
+ selected = unique(selected);
401
+ return {
402
+ hypotheses: [hypothesis],
403
+ capabilities,
404
+ selectedWritableDirect: selected,
405
+ referenceFiles: references,
406
+ blockingWarnings: warnings.filter((item) => item.blocking !== false),
407
+ warnings,
408
+ };
409
+ }
410
+
43
411
  function basenameTokens(file) {
44
412
  return tokenize(path.basename(file || '').replace(/([a-z])([A-Z])/g, '$1 $2'));
45
413
  }
@@ -734,10 +1102,22 @@ function reviewGatedNotices(scope) {
734
1102
  return notices;
735
1103
  }
736
1104
 
737
- function contextMode(area, scope, validationPlan, relevant, decomposition = []) {
1105
+ function hasBlockingMapGapWarning(warnings = []) {
1106
+ return warnings.some((warning) => warning.blocking === true || [
1107
+ 'MISSING_DIRECT_NEW_FILE_SCOPE',
1108
+ 'DIRECT_ONLY_REFERENCE_PEERS',
1109
+ 'BROAD_FALLBACK_ONLY',
1110
+ 'TARGET_ENTITY_MISMATCH',
1111
+ 'MISSING_ARTIFACT_FAMILY_SCOPE',
1112
+ 'MISSING_REQUIRED_HOOK_SCOPE',
1113
+ ].includes(warning.code));
1114
+ }
1115
+
1116
+ function contextMode(area, selectedWritableDirect, validationPlan, relevant, decomposition = [], warnings = []) {
738
1117
  if (!area) return 'needs_map';
739
1118
  if (decomposition.some((item) => item.blocking)) return 'needs_decomposition';
740
- if (scope.writableDirect.length === 0) return 'discovery_context';
1119
+ if (selectedWritableDirect.length === 0) return 'discovery_context';
1120
+ if (hasBlockingMapGapWarning(warnings)) return 'discovery_context';
741
1121
  if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) return 'discovery_context';
742
1122
  if (validationPlan.length === 0) return 'discovery_context';
743
1123
  return 'edit_context';
@@ -850,12 +1230,31 @@ function buildContextMarkdown(packet) {
850
1230
  ...packet.phaseDependencyStatus.missingRequiredAcceptedPhases.map((item) => `- Missing required phase: ${item}`),
851
1231
  '',
852
1232
  ] : []),
1233
+ ...(packet.writePlan ? [
1234
+ '## Write Plan',
1235
+ ...packet.writePlan.hypotheses.map((item) => `- ${item.action} ${item.artifactFamily}: ${item.operation}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''}`),
1236
+ ...(packet.writePlan.blockingWarnings.length
1237
+ ? packet.writePlan.blockingWarnings.map((item) => `- Blocking: ${item.code}: ${item.message}`)
1238
+ : ['- Coverage: direct writable scope covers the inferred write operation.']),
1239
+ '',
1240
+ ] : []),
1241
+ ...(packet.writePlan && packet.writePlan.referenceFiles.length ? [
1242
+ '## Reference Files',
1243
+ ...packet.writePlan.referenceFiles.map((file) => `- ${file.path}: ${file.reason}`),
1244
+ '',
1245
+ ] : []),
853
1246
  '## Relevant Files',
854
1247
  ...packet.relevantFiles.map((file) => `- ${file.path} (${file.role || file.source}): ${file.reason}`),
855
1248
  '',
856
1249
  '## Writable Scope',
857
1250
  ...(packet.writableScope.length ? packet.writableScope.map((item) => `- ${item}`) : ['- (none; this is not an edit permit)']),
858
1251
  '',
1252
+ '## Writable Existing Scope',
1253
+ ...(packet.scope.writableExisting && packet.scope.writableExisting.length ? packet.scope.writableExisting.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
1254
+ '',
1255
+ '## Writable Create Patterns',
1256
+ ...(packet.scope.writableCreatePatterns && packet.scope.writableCreatePatterns.length ? packet.scope.writableCreatePatterns.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
1257
+ '',
859
1258
  '## Conditional Writable Scope',
860
1259
  ...(packet.scope.writableConditional.length ? packet.scope.writableConditional.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
861
1260
  '',
@@ -921,12 +1320,24 @@ function createContextPacket(root, intent, options = {}) {
921
1320
  const candidateAreas = candidateAreasFromScored(scored);
922
1321
  const scope = area ? normalizeAreaScope(root, area) : {
923
1322
  readable: [],
1323
+ readableReference: [],
924
1324
  writableDirect: [],
1325
+ writableDirectItems: [],
1326
+ writableExisting: [],
1327
+ writableCreatePatterns: [],
925
1328
  writableConditional: [],
926
1329
  writableBroadFallback: [],
927
1330
  forbidden: [],
928
1331
  quality: 'unknown',
929
1332
  };
1333
+ const writePlan = area ? buildWritePlan(scope, intent, area) : {
1334
+ hypotheses: [],
1335
+ capabilities: [],
1336
+ selectedWritableDirect: [],
1337
+ referenceFiles: [],
1338
+ blockingWarnings: [],
1339
+ warnings: [],
1340
+ };
930
1341
  const restrictGrepToReadable = area && (confidence === 'high' || findOptions.area) && scope.readable.length > 0;
931
1342
  const grepFiles = grepHints(root, tokens, maxFiles, {
932
1343
  allowedPatterns: restrictGrepToReadable ? scope.readable : [],
@@ -969,6 +1380,7 @@ function createContextPacket(root, intent, options = {}) {
969
1380
  });
970
1381
  } else {
971
1382
  warnings.push(...scopeWarnings(scope));
1383
+ warnings.push(...writePlan.warnings);
972
1384
  if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) {
973
1385
  warnings.push({
974
1386
  code: 'MISSING_ENTRYPOINT',
@@ -996,7 +1408,7 @@ function createContextPacket(root, intent, options = {}) {
996
1408
  constrainedAreaId,
997
1409
  });
998
1410
  const phaseSeeds = decomposition.length > 0 ? candidatePhaseSeeds(scored, intent) : [];
999
- let mode = contextMode(area, scope, validationPlan, relevant, decomposition);
1411
+ let mode = contextMode(area, writePlan.selectedWritableDirect, validationPlan, relevant, decomposition, warnings);
1000
1412
  if (missingSplitReference(phaseStatus) && mode === 'edit_context') mode = 'discovery_context';
1001
1413
  const actions = recommendedActions(mode, intent, id, phaseStatus);
1002
1414
  const packet = {
@@ -1034,7 +1446,8 @@ function createContextPacket(root, intent, options = {}) {
1034
1446
  phaseDependencyStatus: phaseStatus,
1035
1447
  relevantFiles: relevant,
1036
1448
  scope,
1037
- writableScope: scope.writableDirect.slice(),
1449
+ writePlan,
1450
+ writableScope: writePlan.selectedWritableDirect.slice(),
1038
1451
  readableScope: scope.readable.slice(),
1039
1452
  doNotTouch: scope.forbidden.slice(),
1040
1453
  validationPlan,
@@ -1076,6 +1489,7 @@ function createContextPacket(root, intent, options = {}) {
1076
1489
  })),
1077
1490
  validationQuality: validationPlan.length > 0 ? 'configured' : 'missing',
1078
1491
  warnings,
1492
+ writePlan,
1079
1493
  reviewGated,
1080
1494
  decompositionSignals: decomposition,
1081
1495
  candidatePhaseSeeds: phaseSeeds,
@@ -106,6 +106,13 @@ function evaluateEvidencePolicy(change, map) {
106
106
  let changeClass = 'unknown';
107
107
  let verdict = 'acceptable';
108
108
 
109
+ if (change.writePlanStatus && change.writePlanStatus.status === 'blocked') {
110
+ verdict = 'blocked';
111
+ missing.push('write_plan_clean');
112
+ required.push('write_plan_clean');
113
+ reasons.push(...(change.writePlanStatus.blockingReasons || []).map((item) => `Write plan blocked: ${item}`));
114
+ }
115
+
109
116
  if (change.scope.status === 'violation') {
110
117
  verdict = 'blocked';
111
118
  missing.push('scope_clean');
package/src/lib/map.js CHANGED
@@ -331,10 +331,14 @@ function summarizeFindContext(context) {
331
331
  },
332
332
  warnings: context.warnings || [],
333
333
  reviewGated: context.reviewGated || (context.explain && context.explain.reviewGated) || [],
334
+ writePlan: context.writePlan || (context.explain && context.explain.writePlan) || null,
334
335
  candidateAreas: context.candidateAreas || [],
335
336
  areaScores: context.explain && context.explain.areaScores ? context.explain.areaScores : [],
336
337
  scopeQuality: scope.quality || (context.explain ? context.explain.scopeQuality : 'unknown'),
337
338
  writableDirect: scope.writableDirect || context.writableScope || [],
339
+ selectedWritableScope: context.writableScope || [],
340
+ writableExisting: scope.writableExisting || [],
341
+ writableCreatePatterns: scope.writableCreatePatterns || [],
338
342
  writableConditional: scope.writableConditional || [],
339
343
  writableBroadFallback: scope.writableBroadFallback || [],
340
344
  forbidden: scope.forbidden || context.doNotTouch || [],
@@ -431,6 +435,12 @@ function repairFocusFromFind(findContext, userFocus) {
431
435
  if (isGenericAreaId(area)) goals.push(`replace generic area id with a focused id such as ${suggestedArea}`);
432
436
  if (warnings.includes('BROAD_WRITABLE_SCOPE')) goals.push('move broad writable scope to reviewed fallback');
433
437
  if (warnings.includes('MISSING_DIRECT_WRITABLE_SCOPE')) goals.push('add narrow writable_direct files');
438
+ if (warnings.includes('MISSING_DIRECT_NEW_FILE_SCOPE')) goals.push('add narrow writable_direct patterns or exact paths for the new peer files this intent must create');
439
+ if (warnings.includes('DIRECT_ONLY_REFERENCE_PEERS')) goals.push('move peer examples out of selected direct scope and add target create capability');
440
+ if (warnings.includes('BROAD_FALLBACK_ONLY')) goals.push('replace broad fallback-only coverage with narrow direct create capability');
441
+ if (warnings.includes('TARGET_ENTITY_MISMATCH')) goals.push('add direct scope for the requested target identity or mark old peers as references');
442
+ if (warnings.includes('MISSING_ARTIFACT_FAMILY_SCOPE')) goals.push('add writable_create_patterns for required artifact families');
443
+ if (warnings.includes('MISSING_REQUIRED_HOOK_SCOPE')) goals.push('add writable_existing hook files required by the artifact family');
434
444
  if (warnings.includes('MISSING_ENTRYPOINT')) goals.push('add durable entrypoints');
435
445
  if (warnings.includes('MISSING_VALIDATION')) goals.push('add validation profile references');
436
446
  if (warnings.includes('NO_CONFIDENT_AREA')) goals.push('create or refine a focused area');
@@ -464,6 +474,15 @@ function findContextPromptSection(findContext) {
464
474
  'Review-gated scope from find:',
465
475
  ...listLines(findContext.reviewGated, (notice) => `- ${notice.code}: ${notice.message}`),
466
476
  '',
477
+ ...(findContext.writePlan ? [
478
+ 'Write plan from find:',
479
+ ...listLines(findContext.writePlan.hypotheses || [], (item) => `- ${item.action} ${item.artifactFamily}: ${item.operation}${item.targetIdentity ? ` for ${item.targetIdentity}` : ''}`),
480
+ 'Write plan blocking warnings:',
481
+ ...listLines(findContext.writePlan.blockingWarnings || [], (item) => `- ${item.code}: ${item.message}`),
482
+ 'Reference files from write plan:',
483
+ ...listLines(findContext.writePlan.referenceFiles || [], (file) => `- ${file.path}: ${file.reason || ''}`),
484
+ '',
485
+ ] : []),
467
486
  'Candidate/area scores:',
468
487
  ...listLines(findContext.areaScores.length ? findContext.areaScores : findContext.candidateAreas, (area) => {
469
488
  const signals = area.signals && area.signals.length ? ` (${area.signals.slice(0, 3).join(', ')})` : '';
@@ -476,6 +495,15 @@ function findContextPromptSection(findContext) {
476
495
  'Current direct writable scope:',
477
496
  ...listLines(findContext.writableDirect, patternLine),
478
497
  '',
498
+ 'Selected direct writable scope for this intent:',
499
+ ...listLines(findContext.selectedWritableScope, patternLine),
500
+ '',
501
+ 'Current writable existing scope:',
502
+ ...listLines(findContext.writableExisting, patternLine),
503
+ '',
504
+ 'Current writable create patterns:',
505
+ ...listLines(findContext.writableCreatePatterns, patternLine),
506
+ '',
479
507
  'Current conditional writable scope:',
480
508
  ...listLines(findContext.writableConditional, patternLine),
481
509
  '',
@@ -541,6 +569,9 @@ function buildMapPrompt(result) {
541
569
  '- Every `writable_conditional` and `writable_broad_fallback` item must set `requires_review: true`; never output `requires_review: false` for those tiers.',
542
570
  '- Keep broad patterns like `apps/*/src/**` only under `writable_broad_fallback`, never under `writable_direct`.',
543
571
  '- `writable_direct` should use exact existing files unless there is a very small, justified pattern.',
572
+ '- Prefer `writable_existing` for exact update targets and `writable_create_patterns` for new peer files when a repo has create/update distinctions.',
573
+ '- Put copy sources and peer examples under `readable_reference`, not under selected Direct Writable for new peer intents.',
574
+ '- Use `artifact_families` to declare required create artifacts and hook files for provider/adapter/endpoint families.',
544
575
  '- Entrypoint roles must use this fixed taxonomy only: ' + roleListText() + '.',
545
576
  '- Do not invent role names. If unsure, use `role: unknown` plus a TODO note.',
546
577
  '- Add `source` and `confidence` to entrypoints when possible. Use `source: agent_draft` for your own draft and `confidence: low|medium|high`.',
@@ -594,8 +625,27 @@ function buildMapPrompt(result) {
594
625
  ' scope:',
595
626
  ' readable:',
596
627
  ' - path/to/read/**',
628
+ ' readable_reference:',
629
+ ' peer_examples:',
630
+ ' - path: path/to/ExistingPeer.ts',
631
+ ' artifact: service_impl',
632
+ ' peer_identity: Existing',
597
633
  ' writable_direct:',
598
634
  ' - path/to/real/file.ts',
635
+ ' writable_existing:',
636
+ ' - path: path/to/Registry.ts',
637
+ ' artifact: registry_hook',
638
+ ' operations:',
639
+ ' - update_existing',
640
+ ' required_when:',
641
+ ' - add_provider_proxy',
642
+ ' writable_create_patterns:',
643
+ ' - pattern: path/to/*Provider.ts',
644
+ ' artifact: service_impl',
645
+ ' operations:',
646
+ ' - create_file',
647
+ ' intent_kinds:',
648
+ ' - add_provider_proxy',
599
649
  ' writable_conditional:',
600
650
  ' - pattern: path/to/shared/**',
601
651
  ' reason: only if shared model changes are required',
@@ -623,6 +673,19 @@ function buildMapPrompt(result) {
623
673
  ' default_level: low',
624
674
  ' tags:',
625
675
  ' - ui',
676
+ ' artifact_families:',
677
+ ' provider_proxy:',
678
+ ' trigger_terms:',
679
+ ' en:',
680
+ ' - provider',
681
+ ' - proxy',
682
+ ' zh:',
683
+ ' - 提供商',
684
+ ' - 代理',
685
+ ' create_requires:',
686
+ ' - service_impl',
687
+ ' hooks:',
688
+ ' - registry_hook',
626
689
  '```',
627
690
  '',
628
691
  'Validation profile schema example:',
package/src/lib/risk.js CHANGED
@@ -54,6 +54,11 @@ function evaluateChangeRisk(change, map) {
54
54
  if (change.scope.status === 'violation') {
55
55
  blockingReasons.push(`scope violation: ${change.scope.violations.join(', ')}`);
56
56
  }
57
+ if (change.writePlanStatus && change.writePlanStatus.status === 'blocked') {
58
+ for (const reason of change.writePlanStatus.blockingReasons || []) {
59
+ blockingReasons.push(`write plan blocked: ${reason}`);
60
+ }
61
+ }
57
62
 
58
63
  const status = validationStatus(change);
59
64
  if (status === 'failed') blockingReasons.push('validation failed');
package/src/lib/scope.js CHANGED
@@ -12,6 +12,12 @@ function patternFrom(value) {
12
12
  return normalizePath((value.pattern || value.path || '').trim());
13
13
  }
14
14
 
15
+ function arrayFrom(value) {
16
+ if (Array.isArray(value)) return value.map((item) => String(item || '')).filter(Boolean);
17
+ if (value == null || value === '') return [];
18
+ return [String(value)];
19
+ }
20
+
15
21
  function normalizePattern(value, defaults = {}) {
16
22
  const pattern = patternFrom(value);
17
23
  if (!pattern) return null;
@@ -22,6 +28,13 @@ function normalizePattern(value, defaults = {}) {
22
28
  requiresReview: defaults.requiresReview === true,
23
29
  source: defaults.source || 'map',
24
30
  matchCount: defaults.matchCount == null ? null : defaults.matchCount,
31
+ artifact: defaults.artifact || null,
32
+ artifactFamily: defaults.artifactFamily || null,
33
+ operations: arrayFrom(defaults.operations),
34
+ targetIdentity: defaults.targetIdentity || null,
35
+ targetSlot: defaults.targetSlot || null,
36
+ intentKinds: arrayFrom(defaults.intentKinds),
37
+ requiredWhen: arrayFrom(defaults.requiredWhen),
25
38
  };
26
39
  }
27
40
  return {
@@ -30,6 +43,13 @@ function normalizePattern(value, defaults = {}) {
30
43
  requiresReview: value.requires_review === true || value.requiresReview === true || defaults.requiresReview === true,
31
44
  source: value.source || defaults.source || 'map',
32
45
  matchCount: value.matchCount == null ? (defaults.matchCount == null ? null : defaults.matchCount) : value.matchCount,
46
+ artifact: value.artifact || defaults.artifact || null,
47
+ artifactFamily: value.artifact_family || value.artifactFamily || defaults.artifactFamily || null,
48
+ operations: arrayFrom(value.operations || value.operation || defaults.operations),
49
+ targetIdentity: value.target_identity || value.targetIdentity || value.peer_identity || value.peerIdentity || defaults.targetIdentity || null,
50
+ targetSlot: value.target_slot || value.targetSlot || defaults.targetSlot || null,
51
+ intentKinds: arrayFrom(value.intent_kinds || value.intentKinds || defaults.intentKinds),
52
+ requiredWhen: arrayFrom(value.required_when || value.requiredWhen || defaults.requiredWhen),
33
53
  };
34
54
  }
35
55
 
@@ -93,29 +113,119 @@ function splitDirectAndBroad(root, values, defaults = {}) {
93
113
  return { direct, broad };
94
114
  }
95
115
 
116
+ function splitItemsAndBroad(root, values, defaults = {}) {
117
+ const direct = [];
118
+ const broad = [];
119
+ for (const value of values || []) {
120
+ const pattern = patternFrom(value);
121
+ if (!pattern) continue;
122
+ const classification = isBroadPattern(root, pattern);
123
+ const item = normalizePattern(value, {
124
+ ...defaults,
125
+ matchCount: classification.matchCount,
126
+ });
127
+ if (!item) continue;
128
+ if (classification.broad) {
129
+ broad.push({
130
+ ...item,
131
+ reason: item.reason || classification.reason || 'broad writable scope',
132
+ requiresReview: true,
133
+ });
134
+ } else {
135
+ direct.push(item);
136
+ }
137
+ }
138
+ return { direct, broad };
139
+ }
140
+
141
+ function flattenReferenceValues(value) {
142
+ if (!value) return [];
143
+ if (Array.isArray(value)) return value;
144
+ if (typeof value === 'object' && (value.path || value.pattern)) return [value];
145
+ if (typeof value === 'object') {
146
+ return Object.values(value).flatMap((item) => flattenReferenceValues(item));
147
+ }
148
+ return [value];
149
+ }
150
+
151
+ function normalizeReference(value, defaults = {}) {
152
+ const pattern = patternFrom(value);
153
+ if (!pattern) return null;
154
+ if (typeof value === 'string') {
155
+ return {
156
+ path: pattern,
157
+ reason: defaults.reason || '',
158
+ role: defaults.role || 'reference',
159
+ artifact: defaults.artifact || null,
160
+ artifactFamily: defaults.artifactFamily || null,
161
+ peerIdentity: defaults.peerIdentity || null,
162
+ source: defaults.source || 'readable_reference',
163
+ };
164
+ }
165
+ return {
166
+ path: pattern,
167
+ reason: value.reason || defaults.reason || '',
168
+ role: value.role || defaults.role || 'reference',
169
+ artifact: value.artifact || defaults.artifact || null,
170
+ artifactFamily: value.artifact_family || value.artifactFamily || defaults.artifactFamily || null,
171
+ peerIdentity: value.peer_identity || value.peerIdentity || value.provider || defaults.peerIdentity || null,
172
+ source: value.source || defaults.source || 'readable_reference',
173
+ };
174
+ }
175
+
176
+ function normalizeReferenceList(value, defaults = {}) {
177
+ return flattenReferenceValues(value).map((item) => normalizeReference(item, defaults)).filter(Boolean);
178
+ }
179
+
96
180
  function normalizeAreaScope(root, area = {}) {
97
181
  const configured = area.scope || {};
98
182
  const readable = (configured.readable || area.readable_scope || []).map(patternFrom).filter(Boolean);
183
+ const readableReference = normalizeReferenceList(configured.readable_reference || area.readable_reference || [], {
184
+ source: 'readable_reference',
185
+ });
99
186
  const forbidden = (configured.forbidden || area.do_not_touch || []).map(patternFrom).filter(Boolean);
100
187
 
101
188
  const directValues = configured.writable_direct || [];
189
+ const existingValues = configured.writable_existing || [];
190
+ const createValues = configured.writable_create_patterns || [];
102
191
  const conditionalValues = configured.writable_conditional || [];
103
192
  const broadValues = configured.writable_broad_fallback || [];
104
193
  const legacyWritable = !area.scope ? (area.writable_scope || []) : [];
105
194
 
106
- const directSplit = splitDirectAndBroad(root, directValues, { source: 'writable_direct' });
195
+ const directSplit = splitItemsAndBroad(root, directValues, { source: 'writable_direct' });
196
+ const existingSplit = splitItemsAndBroad(root, existingValues, {
197
+ source: 'writable_existing',
198
+ operations: ['update_existing'],
199
+ });
200
+ const createSplit = splitItemsAndBroad(root, createValues, {
201
+ source: 'writable_create_patterns',
202
+ operations: ['create_file'],
203
+ });
107
204
  const legacySplit = splitDirectAndBroad(root, legacyWritable, {
108
205
  source: 'legacy_writable_scope',
109
206
  reason: 'legacy writable_scope is broad; move this to scope.writable_broad_fallback or add writable_direct',
110
207
  });
111
208
 
112
- const writableDirect = [...directSplit.direct, ...legacySplit.direct];
209
+ const legacyDirectItems = legacySplit.direct.map((pattern) => normalizePattern(pattern, {
210
+ source: 'legacy_writable_scope',
211
+ })).filter(Boolean);
212
+ const writableDirectItems = [
213
+ ...directSplit.direct,
214
+ ...existingSplit.direct,
215
+ ...createSplit.direct,
216
+ ...legacyDirectItems,
217
+ ];
218
+ const writableDirect = writableDirectItems.map((item) => item.pattern);
219
+ const writableExisting = existingSplit.direct;
220
+ const writableCreatePatterns = createSplit.direct;
113
221
  const writableConditional = normalizePatternList(conditionalValues, {
114
222
  source: 'writable_conditional',
115
223
  requiresReview: true,
116
224
  });
117
225
  const writableBroadFallback = [
118
226
  ...directSplit.broad,
227
+ ...existingSplit.broad,
228
+ ...createSplit.broad,
119
229
  ...normalizePatternList(broadValues, {
120
230
  source: 'writable_broad_fallback',
121
231
  requiresReview: true,
@@ -139,7 +249,11 @@ function normalizeAreaScope(root, area = {}) {
139
249
 
140
250
  return {
141
251
  readable,
252
+ readableReference,
142
253
  writableDirect,
254
+ writableDirectItems,
255
+ writableExisting,
256
+ writableCreatePatterns,
143
257
  writableConditional,
144
258
  writableBroadFallback,
145
259
  forbidden,
@@ -164,5 +278,6 @@ module.exports = {
164
278
  itemPattern,
165
279
  normalizeAreaScope,
166
280
  normalizePatternList,
281
+ normalizeReferenceList,
167
282
  patternDisplay,
168
283
  };