kotonoha 0.4.1 → 0.4.2
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/CHANGELOG.md +28 -1
- package/README-ja.md +68 -12
- package/README.md +100 -17
- package/bin/kotonoha.js +4 -1
- package/package.json +6 -3
- package/skills/obsidian-bases/SKILL.md +140 -0
- package/skills/obsidian-bases/assets/templates/cards.base +8 -0
- package/skills/obsidian-bases/assets/templates/kanban.base +7 -0
- package/skills/obsidian-bases/assets/templates/list.base +8 -0
- package/skills/obsidian-bases/assets/templates/table.base +10 -0
- package/skills/obsidian-bases/references/execution-contract.json +246 -0
- package/skills/obsidian-bases/references/workflow.md +155 -0
- package/skills/obsidian-bases/scripts/check.cjs +690 -0
- package/skills/obsidian-bases/scripts/package-lock.json +104 -0
- package/skills/obsidian-bases/scripts/package.json +18 -0
- package/skills/obsidian-bases/scripts/verify.cjs +454 -0
- package/skills/obsidian-canvas/SKILL.md +134 -0
- package/skills/obsidian-canvas/assets/templates/comparison.canvas +11 -0
- package/skills/obsidian-canvas/assets/templates/mind-map.canvas +11 -0
- package/skills/obsidian-canvas/assets/templates/note-map.canvas +10 -0
- package/skills/obsidian-canvas/assets/templates/workflow.canvas +11 -0
- package/skills/obsidian-canvas/references/execution-contract.json +404 -0
- package/skills/obsidian-canvas/references/workflow.md +187 -0
- package/skills/obsidian-canvas/scripts/canvas.py +1479 -0
- package/skills/obsidian-canvas/scripts/canvas.py.lock +143 -0
- package/skills/obsidian-canvas/scripts/test_canvas.py +2047 -0
- package/skills/obsidian-canvas/scripts/test_canvas.py.lock +143 -0
- package/skills/obsidian-vault/SKILL.md +149 -93
- package/skills/obsidian-vault/references/workflow.md +59 -4
- package/skills/obsidian-vault/scripts/test_vault.py +76 -0
- package/skills/obsidian-vault/scripts/vault.py +118 -71
- package/skills/tech-writer/SKILL.md +12 -2
|
@@ -40,6 +40,7 @@ CHECKS = (
|
|
|
40
40
|
"actors", "negation", "conditions", "modality", "scope", "causality",
|
|
41
41
|
"quantity_bindings", "terminology", "structure",
|
|
42
42
|
)
|
|
43
|
+
STORAGE_API_VERSION = 1
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class Failure(Exception):
|
|
@@ -166,6 +167,12 @@ class Vault:
|
|
|
166
167
|
raise Failure("outside_scope", path)
|
|
167
168
|
if not path.endswith(".md"):
|
|
168
169
|
raise Failure("unsupported_format", path)
|
|
170
|
+
return self.read_authorized(path, maximum)
|
|
171
|
+
|
|
172
|
+
def read_authorized(self, path, maximum=None):
|
|
173
|
+
path = relative(path)
|
|
174
|
+
if not self.allowed(path):
|
|
175
|
+
raise Failure("outside_scope", path)
|
|
169
176
|
with self.parent(path) as (parent, name):
|
|
170
177
|
try:
|
|
171
178
|
descriptor = os.open(
|
|
@@ -186,7 +193,20 @@ class Vault:
|
|
|
186
193
|
raise Failure("limit_reached", path)
|
|
187
194
|
return data, metadata
|
|
188
195
|
|
|
196
|
+
def stat_authorized(self, path):
|
|
197
|
+
path = relative(path)
|
|
198
|
+
if not self.allowed(path):
|
|
199
|
+
raise Failure("outside_scope", path)
|
|
200
|
+
with self.parent(path) as (parent, name):
|
|
201
|
+
metadata = os.stat(name, dir_fd=parent, follow_symlinks=False)
|
|
202
|
+
if stat.S_ISLNK(metadata.st_mode):
|
|
203
|
+
raise Failure("symlink_not_followed", path)
|
|
204
|
+
return metadata
|
|
205
|
+
|
|
189
206
|
def enumerate(self):
|
|
207
|
+
return self.enumerate_authorized((".md",), strict_file_scopes=True)
|
|
208
|
+
|
|
209
|
+
def enumerate_authorized(self, suffixes, strict_file_scopes=False):
|
|
190
210
|
paths, skipped, issues = set(), set(), []
|
|
191
211
|
enumeration_complete = True
|
|
192
212
|
|
|
@@ -201,7 +221,7 @@ class Vault:
|
|
|
201
221
|
return
|
|
202
222
|
for name in names:
|
|
203
223
|
path = name if prefix == "." else prefix + "/" + name
|
|
204
|
-
if name in MANAGEMENT:
|
|
224
|
+
if name in MANAGEMENT or not strict_file_scopes and name.lower() in MANAGEMENT:
|
|
205
225
|
issues.append(issue("excluded_management_directory", path))
|
|
206
226
|
continue
|
|
207
227
|
try:
|
|
@@ -217,7 +237,7 @@ class Vault:
|
|
|
217
237
|
walk(child, path)
|
|
218
238
|
finally:
|
|
219
239
|
os.close(child)
|
|
220
|
-
elif name.endswith(
|
|
240
|
+
elif name.endswith(tuple(suffixes)):
|
|
221
241
|
if stat.S_ISREG(metadata.st_mode):
|
|
222
242
|
paths.add(path)
|
|
223
243
|
else:
|
|
@@ -230,8 +250,10 @@ class Vault:
|
|
|
230
250
|
|
|
231
251
|
for kind, path in self.scopes:
|
|
232
252
|
if kind == "file":
|
|
233
|
-
if not path.endswith(
|
|
234
|
-
|
|
253
|
+
if not path.endswith(tuple(suffixes)):
|
|
254
|
+
if strict_file_scopes:
|
|
255
|
+
raise Failure("unsupported_format", path)
|
|
256
|
+
continue
|
|
235
257
|
paths.add(path)
|
|
236
258
|
continue
|
|
237
259
|
try:
|
|
@@ -980,6 +1002,75 @@ def exclusive_create(parent, stage_name, name):
|
|
|
980
1002
|
os.link(stage_name, name, src_dir_fd=parent, dst_dir_fd=parent, follow_symlinks=False)
|
|
981
1003
|
|
|
982
1004
|
|
|
1005
|
+
def commit_bytes(vault, parent, name, stage_name, before, candidate, metadata,
|
|
1006
|
+
vault_identity, progress, *, read_at, target):
|
|
1007
|
+
"""Commit validated bytes; retain commit progress even when verification fails."""
|
|
1008
|
+
progress.update(committed=False, own_stage=False, cleanup_failed=False)
|
|
1009
|
+
try:
|
|
1010
|
+
try:
|
|
1011
|
+
descriptor = os.open(
|
|
1012
|
+
stage_name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
1013
|
+
0o666 if before is None else 0o600, dir_fd=parent,
|
|
1014
|
+
)
|
|
1015
|
+
except FileExistsError as error:
|
|
1016
|
+
raise Failure("target_exists") from error
|
|
1017
|
+
progress["own_stage"] = True
|
|
1018
|
+
with os.fdopen(descriptor, "w+b") as stream:
|
|
1019
|
+
stream.write(candidate)
|
|
1020
|
+
stream.flush()
|
|
1021
|
+
if metadata is not None:
|
|
1022
|
+
os.fchown(stream.fileno(), metadata.st_uid, metadata.st_gid)
|
|
1023
|
+
os.fchmod(stream.fileno(), stat.S_IMODE(metadata.st_mode))
|
|
1024
|
+
os.fsync(stream.fileno())
|
|
1025
|
+
current_root = directory(vault.root)
|
|
1026
|
+
try:
|
|
1027
|
+
if identity(os.fstat(current_root)) != vault_identity:
|
|
1028
|
+
raise Failure("parent_changed")
|
|
1029
|
+
finally:
|
|
1030
|
+
os.close(current_root)
|
|
1031
|
+
with vault.parent(target) as (current_parent, _):
|
|
1032
|
+
if identity(os.fstat(current_parent)) != identity(os.fstat(parent)):
|
|
1033
|
+
raise Failure("parent_changed", target)
|
|
1034
|
+
if before is not None:
|
|
1035
|
+
current, current_metadata = read_at(parent, name)
|
|
1036
|
+
if digest(current) != digest(before):
|
|
1037
|
+
raise Failure("target_changed", target)
|
|
1038
|
+
if (identity(current_metadata) != identity(metadata)
|
|
1039
|
+
or current_metadata.st_mode != metadata.st_mode
|
|
1040
|
+
or current_metadata.st_gid != metadata.st_gid):
|
|
1041
|
+
raise Failure("target_changed", target)
|
|
1042
|
+
with os.fdopen(os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=parent), "rb") as check:
|
|
1043
|
+
metadata_supported(os.fstat(check.fileno()), check.fileno())
|
|
1044
|
+
os.replace(stage_name, name, src_dir_fd=parent, dst_dir_fd=parent)
|
|
1045
|
+
progress.update(committed=True, own_stage=False)
|
|
1046
|
+
else:
|
|
1047
|
+
try:
|
|
1048
|
+
exclusive_create(parent, stage_name, name)
|
|
1049
|
+
except FileExistsError as error:
|
|
1050
|
+
raise Failure("target_exists", target) from error
|
|
1051
|
+
progress["committed"] = True
|
|
1052
|
+
os.fsync(parent)
|
|
1053
|
+
verified, _ = read_at(parent, name)
|
|
1054
|
+
if digest(verified) != digest(candidate):
|
|
1055
|
+
raise OSError("Post-commit verification failed")
|
|
1056
|
+
if progress["own_stage"]:
|
|
1057
|
+
os.unlink(stage_name, dir_fd=parent)
|
|
1058
|
+
progress["own_stage"] = False
|
|
1059
|
+
os.fsync(parent)
|
|
1060
|
+
return verified
|
|
1061
|
+
finally:
|
|
1062
|
+
original_error = sys.exc_info()[1]
|
|
1063
|
+
if progress["own_stage"]:
|
|
1064
|
+
try:
|
|
1065
|
+
os.unlink(stage_name, dir_fd=parent)
|
|
1066
|
+
except OSError:
|
|
1067
|
+
progress["cleanup_failed"] = True
|
|
1068
|
+
if original_error is None:
|
|
1069
|
+
raise
|
|
1070
|
+
else:
|
|
1071
|
+
progress["own_stage"] = False
|
|
1072
|
+
|
|
1073
|
+
|
|
983
1074
|
def apply(request, vault, report, args):
|
|
984
1075
|
if not request["consent"]["workspace_copies"]:
|
|
985
1076
|
raise Failure("consent_missing")
|
|
@@ -1142,77 +1233,33 @@ def apply(request, vault, report, args):
|
|
|
1142
1233
|
pending = dict(report, status="failed", exit_code=1, write_status="unknown",
|
|
1143
1234
|
issues=[issue("commit_outcome_uncertain", target)])
|
|
1144
1235
|
replace_private(result_path, canonical(pending))
|
|
1145
|
-
committed
|
|
1236
|
+
committed = False
|
|
1237
|
+
progress = {"committed": False, "own_stage": False, "cleanup_failed": False}
|
|
1146
1238
|
stage_name = PurePosixPath(stage).name
|
|
1147
1239
|
try:
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
os.fchown(stream.fileno(), metadata.st_uid, metadata.st_gid)
|
|
1161
|
-
os.fchmod(stream.fileno(), stat.S_IMODE(metadata.st_mode))
|
|
1162
|
-
os.fsync(stream.fileno())
|
|
1163
|
-
current_root = directory(vault.root)
|
|
1164
|
-
try:
|
|
1165
|
-
if identity(os.fstat(current_root)) != planned["vault_identity"]:
|
|
1166
|
-
raise Failure("parent_changed", target)
|
|
1167
|
-
finally:
|
|
1168
|
-
os.close(current_root)
|
|
1169
|
-
with vault.parent(target) as (current_parent, _):
|
|
1170
|
-
if identity(os.fstat(current_parent)) != identity(os.fstat(parent)):
|
|
1171
|
-
raise Failure("parent_changed", target)
|
|
1172
|
-
if before is not None:
|
|
1173
|
-
current, current_metadata = read_at(parent, name)
|
|
1174
|
-
if digest(current) != planned["before_sha256"]:
|
|
1175
|
-
raise Failure("target_changed", target)
|
|
1176
|
-
if (identity(current_metadata) != identity(metadata)
|
|
1177
|
-
or current_metadata.st_mode != metadata.st_mode
|
|
1178
|
-
or current_metadata.st_gid != metadata.st_gid):
|
|
1179
|
-
raise Failure("target_changed", target)
|
|
1180
|
-
with os.fdopen(os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=parent), "rb") as check:
|
|
1181
|
-
metadata_supported(os.fstat(check.fileno()), check.fileno())
|
|
1182
|
-
os.replace(stage_name, name, src_dir_fd=parent, dst_dir_fd=parent)
|
|
1183
|
-
committed, own_stage = True, False
|
|
1184
|
-
else:
|
|
1185
|
-
try:
|
|
1186
|
-
exclusive_create(parent, stage_name, name)
|
|
1187
|
-
except FileExistsError as error:
|
|
1188
|
-
raise Failure("target_exists", target) from error
|
|
1189
|
-
committed = True
|
|
1190
|
-
os.fsync(parent)
|
|
1191
|
-
verified, _ = read_at(parent, name)
|
|
1192
|
-
if digest(verified) != planned["after_sha256"]:
|
|
1193
|
-
raise OSError("Post-commit verification failed")
|
|
1194
|
-
if own_stage:
|
|
1195
|
-
os.unlink(stage_name, dir_fd=parent)
|
|
1196
|
-
own_stage = False
|
|
1197
|
-
os.fsync(parent)
|
|
1198
|
-
replace_private(receipt_path, canonical(expected_receipt))
|
|
1199
|
-
report.update(operation_state="saved", write_status="saved")
|
|
1200
|
-
report["data"].update(
|
|
1201
|
-
after_sha256=digest(verified), changed=True, verified=True,
|
|
1202
|
-
)
|
|
1203
|
-
report["issues"].append(issue("saved", target))
|
|
1204
|
-
replace_private(result_path, canonical(report))
|
|
1240
|
+
verified = commit_bytes(
|
|
1241
|
+
vault, parent, name, stage_name, before, candidate, metadata,
|
|
1242
|
+
planned["vault_identity"], progress, read_at=read_at, target=target,
|
|
1243
|
+
)
|
|
1244
|
+
committed = progress["committed"]
|
|
1245
|
+
replace_private(receipt_path, canonical(expected_receipt))
|
|
1246
|
+
report.update(operation_state="saved", write_status="saved")
|
|
1247
|
+
report["data"].update(
|
|
1248
|
+
after_sha256=digest(verified), changed=True, verified=True,
|
|
1249
|
+
)
|
|
1250
|
+
report["issues"].append(issue("saved", target))
|
|
1251
|
+
replace_private(result_path, canonical(report))
|
|
1205
1252
|
except (OSError, Failure) as error:
|
|
1253
|
+
committed = progress["committed"]
|
|
1206
1254
|
report["issues"] = [item for item in report["issues"] if item["code"] != "saved"]
|
|
1207
|
-
if
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
report["issues"].append(issue("
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
raise Failure("write_failed_before_commit", target, status="failed", exit_code=1) from cleanup_error
|
|
1255
|
+
if progress["cleanup_failed"]:
|
|
1256
|
+
report["issues"].append(issue("runtime_error", stage))
|
|
1257
|
+
if not committed:
|
|
1258
|
+
report.update(status="failed", exit_code=1, operation_state="blocked")
|
|
1259
|
+
report["issues"].append(issue("write_failed_before_commit", target))
|
|
1260
|
+
raise Failure(
|
|
1261
|
+
"write_failed_before_commit", target, status="failed", exit_code=1,
|
|
1262
|
+
) from error
|
|
1216
1263
|
report.update(
|
|
1217
1264
|
status="failed" if committed or not isinstance(error, Failure) else error.status,
|
|
1218
1265
|
exit_code=1 if committed or not isinstance(error, Failure) else error.exit_code,
|
|
@@ -58,9 +58,19 @@ YAML frontmatter and a few platform extensions on top — see their doctype
|
|
|
58
58
|
reference files. Obsidian uses YAML properties, wikilinks, and callouts;
|
|
59
59
|
see `references/doctypes/obsidian.md` for core Templates compatibility.
|
|
60
60
|
For an existing Vault's conventions, related-note suggestions, link diagnosis,
|
|
61
|
-
or
|
|
61
|
+
or authorized direct edits or CLI plan-approved saves, use the sibling
|
|
62
|
+
`obsidian-vault` skill when available.
|
|
62
63
|
Tech-writer still owns document structure and the reusable template; it must
|
|
63
|
-
|
|
64
|
+
hand Vault writes back to that skill's selected workflow, not bypass its scope
|
|
65
|
+
checks or write into the Vault on its own.
|
|
66
|
+
Preserve the user's working directory when handing off: use a user-specified
|
|
67
|
+
directory, or the current directory at request start if none was specified.
|
|
68
|
+
Resolve the final target there without reinterpreting an explicitly absolute or
|
|
69
|
+
Vault-relative path. Do not replace the target with a copy in session-state
|
|
70
|
+
storage; retain Vault scope checks and separate consent for any auxiliary copies.
|
|
71
|
+
Route standalone `.base` configuration to `obsidian-bases`, not the Markdown
|
|
72
|
+
template or Vault Markdown writer; its host workflow validates without evaluating
|
|
73
|
+
formulas and never mutates source-note properties.
|
|
64
74
|
|
|
65
75
|
## Division of labor
|
|
66
76
|
|