opennori 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.
@@ -154,6 +154,11 @@ the root `nori` Skill routes natural-language requests to focused Skills:
154
154
  `opennori install --skill` installs the pack under `.agents/skills/`. The manifest records `skill_pack`
155
155
  state, and `opennori doctor` checks whether the pack is installed and in sync.
156
156
 
157
+ When upgrading an existing OpenNori project, upgrade entry assets first, then run `opennori check`.
158
+ `check` validates active Nori Contracts and reports `acceptance_quality` warnings for vague ACs
159
+ such as "modify profile fields" or "show an error". It does not rewrite existing contracts, evidence,
160
+ reports, archives, or brainstorms. The user decides whether to revise affected criteria.
161
+
157
162
  ## Nori Profile
158
163
 
159
164
  Acceptance criteria remain human-facing outcomes. A Nori Profile is separate execution
@@ -249,11 +254,12 @@ On every turn:
249
254
  8. After approval, run `opennori approve --root <repo> --summary "<approval>" --json`.
250
255
  9. If the user states required Skills, preferred stacks, avoided tools, install policy, or execution constraints, run `opennori profile add --root <repo> ... --json` and keep those items out of the user acceptance criteria.
251
256
  10. If the user revises a criterion later, run `opennori criterion update --root <repo> --criterion <id> ... --json`; old evidence for the changed criterion is cleared.
252
- 11. Run `opennori resume --root <repo>` or `opennori next --root <repo>` to recover the active goal and current acceptance gap from repository files.
253
- 12. Work only to produce evidence for that gap.
254
- 13. Add acceptance evidence with `opennori evidence add`; choose any suitable verification method, but record basis, sources, reviewability, confidence, and limitations. Add profile compliance evidence with `opennori profile evidence` when profile items exist.
255
- 14. Run `opennori evaluate`.
256
- 15. Report acceptance state, profile compliance, and evidence, not implementation steps.
257
+ 11. If the user asks to upgrade an existing OpenNori project, run `opennori doctor`, preview and confirm `opennori upgrade`, then run `opennori check`; ask the user before revising any existing AC flagged by `acceptance_quality`.
258
+ 12. Run `opennori resume --root <repo>` or `opennori next --root <repo>` to recover the active goal and current acceptance gap from repository files.
259
+ 13. Work only to produce evidence for that gap.
260
+ 14. Add acceptance evidence with `opennori evidence add`; choose any suitable verification method, but record basis, sources, reviewability, confidence, and limitations. Add profile compliance evidence with `opennori profile evidence` when profile items exist.
261
+ 15. Run `opennori evaluate`.
262
+ 16. Report acceptance state, profile compliance, and evidence, not implementation steps.
257
263
 
258
264
  Useful commands:
259
265
 
@@ -269,7 +275,9 @@ Useful commands:
269
275
  - `opennori profile show --root <repo>`: show profile compliance and blocking items.
270
276
  - `opennori list --root <repo>`: list active OpenNori goals.
271
277
  - `opennori install --root <repo>`: create or refresh project-local OpenNori assets and manifest.
278
+ - `opennori upgrade --root <repo>`: preview and refresh project-local OpenNori assets without rewriting active contracts or evidence.
272
279
  - `opennori doctor --root <repo>`: inspect project OpenNori health and recovery actions.
280
+ - `opennori check --root <repo>`: validate active contract structure and audit active ACs for underspecified acceptance quality.
273
281
  - `opennori resume --root <repo>`: recover the active goal, current gap, completion answer, and intervention state.
274
282
  - `opennori status --root <repo>`: answer whether the goal is complete and whether the user needs to act.
275
283
  - `opennori report --root <repo>`: generate the human acceptance report.
package/README.md CHANGED
@@ -94,6 +94,8 @@ language requests to the deterministic CLI state layer.
94
94
  returns structured JSON for agents and automation.
95
95
  - `discover` finds underspecified acceptance gaps before draft, so vague ACs such as "modify fields"
96
96
  or "show an error" become user questions instead of weak contracts.
97
+ - Existing projects keep their Nori Contracts and evidence during upgrade. After upgrading, `check`
98
+ audits active contracts for vague ACs and asks the user before any revision.
97
99
  - `doctor` reports whether project state is `ready`, `needs-action`, or `broken`, with recovery
98
100
  actions.
99
101
  - Nori Profile records required Skills, preferred stacks, avoided tools, and install policy without
@@ -97,9 +97,9 @@
97
97
  {
98
98
  "id": "AC-P-12",
99
99
  "layer": "protocol",
100
- "user_story": "作为用户,我运行 opennori check 后,能发现 AC 是否缺少真实用户操作或可观察结果,而不是只检查是否以“作为用户”开头。",
101
- "measurement": "对包含“测试通过”“字段存在”等内部完成条件的验收草案运行 opennori check,再对包含真实用户操作和报告结果的草案运行 opennori check。",
102
- "threshold": "内部完成条件会被拒绝;包含用户运行、打开、查看、确认等操作,并说明用户能看到或判断什么结果的 AC 可以通过。",
100
+ "user_story": "作为用户,我运行 opennori check 或让 agent 升级已使用 OpenNori 的项目后,能发现现有 active contract 里过于含糊的 AC,而不是让旧的弱验收标准静默通过。",
101
+ "measurement": "对已有 active goal 中包含“修改字段”“失败时有提示”等空泛完成条件的 Nori Contract 运行 opennori check;再对结构正确且包含具体用户操作、字段范围、反馈和判断方式的 contract 运行 check。",
102
+ "threshold": "check 在不改写历史 contract/evidence 的前提下输出 acceptance_quality 审计结果、warnings 和下一步建议;弱 AC 会标出缺少的字段范围、校验规则、成功反馈、失败场景或范围边界;具体 AC 不产生质量告警。",
103
103
  "risk": "high"
104
104
  },
105
105
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opennori",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "OpenNori: human-centered Nori Contracts and evidence records for coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -39,6 +39,7 @@ const NORI_CAPABILITIES = [
39
39
  "skill-pack",
40
40
  "brainstorm",
41
41
  "acceptance-discovery",
42
+ "acceptance-quality-audit",
42
43
  "capability-profile",
43
44
  "profile-check",
44
45
  "archive",
@@ -428,30 +429,40 @@ function sentenceHasSpecifics(text, terms) {
428
429
  return terms.some((term) => value.includes(term.toLowerCase()));
429
430
  }
430
431
 
431
- function discoverAcceptance(goal, explicitId = undefined) {
432
- const text = String(goal || "").trim();
432
+ function discoverAcceptanceGaps(text, { fallback = false, allowedIds = null } = {}) {
433
433
  const lowered = text.toLowerCase();
434
434
  const gaps = DISCOVERY_GAPS
435
+ .filter((gap) => !allowedIds || allowedIds.has(gap.id))
435
436
  .filter((gap) => gap.patterns.some((pattern) => lowered.includes(pattern.toLowerCase())))
436
437
  .filter((gap) => {
437
- if (gap.id === "missing-field-scope") return !sentenceHasSpecifics(text, ["昵称", "头像", "简介", "邮箱", "手机号", "name", "avatar", "bio", "email", "phone"]);
438
- if (gap.id === "missing-validation-rule") return !sentenceHasSpecifics(text, ["长度", "必填", "格式", "大小", "类型", "字符", "required", "format", "length", "size", "type"]);
438
+ if (gap.id === "missing-field-scope") return !sentenceHasSpecifics(text, ["昵称", "头像", "简介", "邮箱", "手机号", "字段范围", "field scope", "name", "avatar", "bio", "email", "phone"]);
439
+ if (gap.id === "missing-validation-rule") return !sentenceHasSpecifics(text, ["长度", "必填", "格式", "大小", "类型", "字符", "校验规则", "validation", "required", "format", "length", "size", "type"]);
439
440
  if (gap.id === "missing-success-signal") return !sentenceHasSpecifics(text, ["成功", "保存成功", "成功反馈", "result", "success"]);
440
441
  if (gap.id === "missing-persistence-scope") return !sentenceHasSpecifics(text, ["刷新", "重新打开", "重新登录", "跨设备", "refresh", "reload", "reopen", "login"]);
441
- if (gap.id === "missing-failure-case") return !sentenceHasSpecifics(text, ["网络", "权限", "无效", "错误码", "保留原", "network", "permission", "invalid"]);
442
- if (gap.id === "missing-out-of-scope-boundary") return !sentenceHasSpecifics(text, ["不在范围", "不包含", "本轮不", "out of scope", "exclude"]);
442
+ if (gap.id === "missing-failure-case") return !sentenceHasSpecifics(text, ["网络", "权限", "无效", "错误码", "保留原", "失败场景", "network", "permission", "invalid"]);
443
+ if (gap.id === "missing-out-of-scope-boundary") return !sentenceHasSpecifics(text, ["不在范围", "不包含", "本轮不", "范围边界", "out of scope", "exclude"]);
443
444
  if (gap.id === "missing-user-entry") return !sentenceHasSpecifics(text, ["设置页", "登录页", "report", "dashboard", "页面", "page"]);
444
445
  if (gap.id === "missing-review-method") return !sentenceHasSpecifics(text, ["截图", "浏览器", "报告", "测试", "review", "screenshot", "browser", "report"]);
445
446
  return true;
446
447
  });
447
448
 
448
- const selectedGaps = gaps.length > 0 ? gaps : [
449
+ if (gaps.length > 0 || !fallback) return gaps;
450
+ return [
449
451
  {
450
452
  id: "missing-review-method",
451
453
  question: "用户或评审者应该用什么可复查方式判断这个目标完成?",
452
454
  why: "OpenNori 需要先知道完成判断方式,才能形成真正可验收的 AC。"
453
455
  }
454
456
  ];
457
+ }
458
+
459
+ function discoveryGap(gapId) {
460
+ return DISCOVERY_GAPS.find((gap) => gap.id === gapId);
461
+ }
462
+
463
+ function discoverAcceptance(goal, explicitId = undefined) {
464
+ const text = String(goal || "").trim();
465
+ const selectedGaps = discoverAcceptanceGaps(text, { fallback: true });
455
466
 
456
467
  return {
457
468
  protocol_version: "opennori/discovery-v1",
@@ -469,6 +480,84 @@ function discoverAcceptance(goal, explicitId = undefined) {
469
480
  };
470
481
  }
471
482
 
483
+ function auditAcceptanceQuality(contract) {
484
+ const findings = [];
485
+ for (const [index, criterion] of (contract.criteria || []).entries()) {
486
+ const triggerText = [
487
+ criterion.user_story,
488
+ criterion.threshold
489
+ ].filter(Boolean).join("\n");
490
+ const fullText = [
491
+ criterion.user_story,
492
+ criterion.measurement,
493
+ criterion.threshold
494
+ ].filter(Boolean).join("\n");
495
+ const addFinding = (gapId) => {
496
+ const gap = discoveryGap(gapId);
497
+ if (!gap) return;
498
+ findings.push({
499
+ criterion_id: criterion.id,
500
+ path: `criteria[${index}]`,
501
+ gap_id: gap.id,
502
+ question: gap.question,
503
+ why: gap.why,
504
+ severity: "needs-user-review"
505
+ });
506
+ };
507
+
508
+ const vagueEditableProfile = sentenceHasSpecifics(triggerText, [
509
+ "修改个人资料",
510
+ "修改资料",
511
+ "修改字段",
512
+ "编辑个人资料",
513
+ "编辑资料",
514
+ "edit profile",
515
+ "update profile",
516
+ "modify fields",
517
+ "edit fields"
518
+ ]);
519
+ if (vagueEditableProfile && !sentenceHasSpecifics(fullText, ["昵称", "头像", "简介", "邮箱", "手机号", "字段范围", "field scope", "name", "avatar", "bio", "email", "phone"])) {
520
+ addFinding("missing-field-scope");
521
+ }
522
+ if (vagueEditableProfile && !sentenceHasSpecifics(fullText, ["长度", "必填", "格式", "大小", "类型", "字符", "校验规则", "validation", "required", "format", "length", "size", "type"])) {
523
+ addFinding("missing-validation-rule");
524
+ }
525
+
526
+ const mentionsSave = sentenceHasSpecifics(triggerText, ["保存", "提交", "save", "submit"]);
527
+ if (mentionsSave && !sentenceHasSpecifics(fullText, ["保存成功", "成功反馈", "成功提示", "显示成功", "报告显示", "result", "success"])) {
528
+ addFinding("missing-success-signal");
529
+ }
530
+ if (mentionsSave && !sentenceHasSpecifics(fullText, ["刷新", "重新打开", "重新登录", "跨设备", "refresh", "reload", "reopen", "login"])) {
531
+ addFinding("missing-persistence-scope");
532
+ }
533
+
534
+ const vagueFailure = sentenceHasSpecifics(triggerText, [
535
+ "失败时有提示",
536
+ "失败提示",
537
+ "有提示",
538
+ "错误提示",
539
+ "show an error",
540
+ "error message"
541
+ ]);
542
+ if (vagueFailure && !sentenceHasSpecifics(fullText, ["网络", "权限", "无效", "错误码", "保留原", "失败场景", "network", "permission", "invalid"])) {
543
+ addFinding("missing-failure-case");
544
+ }
545
+
546
+ const broadSettingsScope = sentenceHasSpecifics(triggerText, ["设置页", "个人资料", "settings page"]);
547
+ if (broadSettingsScope && !sentenceHasSpecifics(fullText, ["不在范围", "不包含", "本轮不", "范围边界", "out of scope", "exclude"])) {
548
+ addFinding("missing-out-of-scope-boundary");
549
+ }
550
+ }
551
+
552
+ return {
553
+ status: findings.length > 0 ? "needs-user-review" : "clear",
554
+ summary: findings.length > 0
555
+ ? `${findings.length} acceptance quality gap(s) may need user review.`
556
+ : "No underspecified acceptance gaps found.",
557
+ findings
558
+ };
559
+ }
560
+
472
561
  function printJson(payload) {
473
562
  console.log(JSON.stringify(payload, null, 2));
474
563
  }
@@ -763,16 +852,20 @@ const SKILL_PACK = [
763
852
  "- Confirm first-time setup after user approval: `opennori bootstrap --root <repo> --confirm --json`.",
764
853
  "- Preview install: `opennori install --root <repo> --dry-run --json`.",
765
854
  "- Install Skill Pack: `opennori install --root <repo> --skill --json`.",
855
+ "- Preview upgrade: `opennori upgrade --root <repo> --skill --dry-run --json`.",
856
+ "- Confirm upgrade after user approval: `opennori upgrade --root <repo> --skill --confirm --json`.",
766
857
  "- Preview destructive install: `opennori install --root <repo> --skill --force --dry-run --json`.",
767
858
  "- Confirm destructive install: `opennori install --root <repo> --skill --force --confirm --json`.",
768
859
  "- Doctor: `opennori doctor --root <repo> --json`.",
860
+ "- Existing contract check after upgrade: `opennori check --root <repo> --json`.",
769
861
  "- Preview uninstall: `opennori uninstall --root <repo> --dry-run --json`.",
770
862
  "- Remove entry assets while preserving state: `opennori uninstall --root <repo> --confirm --json`.",
771
863
  "- Remove all OpenNori state only after explicit user acceptance: `opennori uninstall --root <repo> --include-state --confirm --json`.",
772
864
  "",
773
865
  "## Rules",
774
866
  "Always show dry-run plans before destructive writes.",
775
- "Default uninstall preserves active goals, evidence, reports, archives, and brainstorms."
867
+ "Default uninstall preserves active goals, evidence, reports, archives, and brainstorms.",
868
+ "Upgrade must preserve existing active contracts and evidence. After upgrade, run `opennori check` and route any `acceptance_quality` warnings to `nori-acceptance` for user-approved revision."
776
869
  ]
777
870
  },
778
871
  {
@@ -1867,6 +1960,10 @@ export async function main(args) {
1867
1960
  writeManifest(root);
1868
1961
  }
1869
1962
 
1963
+ const nextActions = dryRun
1964
+ ? ["Review the upgrade plan, then rerun with --confirm if the planned updates are acceptable."]
1965
+ : ["Run opennori check --root <project> --json to audit existing active Nori Contracts for underspecified ACs before continuing work."];
1966
+
1870
1967
  printJson(ok({
1871
1968
  root,
1872
1969
  dry_run: dryRun,
@@ -1874,7 +1971,7 @@ export async function main(args) {
1874
1971
  upgrade_plan: upgradePlan,
1875
1972
  actions: upgradePlan.actions,
1876
1973
  manifest: dryRun ? buildManifest(root) : safeReadManifest(root)
1877
- }));
1974
+ }, [], [], nextActions));
1878
1975
  return;
1879
1976
  }
1880
1977
 
@@ -2029,12 +2126,23 @@ export async function main(args) {
2029
2126
  process.exitCode = 1;
2030
2127
  return;
2031
2128
  }
2129
+ const acceptanceQuality = auditAcceptanceQuality(contract);
2130
+ const warnings = acceptanceQuality.findings.map((finding) => ({
2131
+ type: "acceptance_quality",
2132
+ criterion_id: finding.criterion_id,
2133
+ gap_id: finding.gap_id,
2134
+ message: finding.question
2135
+ }));
2136
+ const nextActions = acceptanceQuality.status === "needs-user-review"
2137
+ ? ["Ask the user the acceptance_quality questions, then revise the affected criteria before relying on this contract as complete."]
2138
+ : [];
2032
2139
  printJson(ok({
2033
2140
  goal_id: contract.goal_id,
2034
2141
  workflow_status: ledger.status,
2035
2142
  current_gap: currentGap(contract, ledger),
2036
- statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status]))
2037
- }));
2143
+ statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status])),
2144
+ acceptance_quality: acceptanceQuality
2145
+ }, [], warnings, nextActions));
2038
2146
  return;
2039
2147
  }
2040
2148